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

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


The following commit(s) were added to refs/heads/master by this push:
     new 09e7e9d593 [SYNCOPE-1967] Support for CAS Attribute Release Consent 
(#1378)
09e7e9d593 is described below

commit 09e7e9d5933c56917dc3d1ad38c4b1d226d5a7a2
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Wed May 6 16:06:54 2026 +0200

    [SYNCOPE-1967] Support for CAS Attribute Release Consent (#1378)
---
 .../authprofiles/AuthProfileDirectoryPanel.java    |  80 +++++++-
 .../AuthProfileItemDirectoryPanel.java             |  14 +-
 .../console/authprofiles/AuthProfilePanel.java     |   5 +-
 .../authprofiles/AuthProfileWizardBuilder.java     |  54 ++++-
 .../client/console/commons/AMConstants.java        |   3 +
 .../apache/syncope/client/console/pages/WA.java    |   2 +-
 .../client/console/rest/AuthProfileRestClient.java |  40 ++++
 .../AuthProfileDirectoryPanel.properties           |   6 +
 .../AuthProfileDirectoryPanel_fr_CA.properties     |   6 +
 .../AuthProfileDirectoryPanel_it.properties        |   6 +
 .../AuthProfileDirectoryPanel_ja.properties        |   6 +
 .../AuthProfileDirectoryPanel_pt_BR.properties     |   6 +
 .../AuthProfileDirectoryPanel_ru.properties        |   6 +
 ...AuthProfileWizardBuilder$ConsentAttributes.html |  23 +++
 .../syncope/client/enduser/pages/AuthProfile.java  |  65 ++++++
 .../client/enduser/rest/AuthProfileRestClient.java |  41 ++++
 .../syncope/client/enduser/pages/AuthProfile.html  |  52 ++++-
 .../client/enduser/pages/AuthProfile.properties    |   4 +
 .../client/enduser/pages/AuthProfile_it.properties |   4 +
 .../client/enduser/pages/AuthProfile_ja.properties |   4 +
 .../enduser/pages/AuthProfile_pt_BR.properties     |   4 +
 .../client/enduser/pages/AuthProfile_ru.properties |   4 +
 .../ui/commons/markup/html/form/AlertBehavior.java |  68 +++++++
 .../wicket/markup/html/form/JsonEditorPanel.java   |   4 -
 .../syncope/common/lib/to/AuthProfileTO.java       |  62 +++++-
 .../syncope/common/lib/wa/WAConsentDecision.java   | 226 +++++++++++++++++++++
 .../api/service/wa/ConsentDecisionService.java     |  84 ++++++++
 .../apache/syncope/core/logic/AMLogicContext.java  |  11 +
 .../core/logic/wa/ConsentDecisionLogic.java        | 112 ++++++++++
 .../syncope/core/rest/cxf/AMRESTCXFContext.java    |  11 +
 .../cxf/service/wa/ConsentDecisionServiceImpl.java |  79 +++++++
 .../persistence/api/entity/am/AuthProfile.java     |   5 +
 .../converters/WAConsentDecisionListConverter.java |  33 ++-
 .../persistence/jpa/entity/am/JPAAuthProfile.java  |  17 ++
 .../neo4j/entity/am/Neo4jAuthProfile.java          |  24 +++
 .../java/data/AuthProfileDataBinderImpl.java       |  21 +-
 .../src/main/resources/wa-embedded.properties      |   2 +-
 .../reference-guide/concepts/authprofile.adoc      |  28 +++
 .../reference-guide/concepts/concepts.adoc         |   2 +
 wa/starter/pom.xml                                 |   4 +
 .../syncope/wa/starter/SyncopeWAApplication.java   |   4 +-
 .../syncope/wa/starter/config/WAContext.java       |  56 +++--
 .../wa/starter/consent/WAConsentRepository.java    | 124 +++++++++++
 .../gauth/WAGoogleMfaAuthCredentialRepository.java |  62 +++---
 wa/starter/src/main/resources/wa.properties        |   2 +-
 .../src/test/resources/debug/wa-debug.properties   |   2 +-
 46 files changed, 1377 insertions(+), 101 deletions(-)

diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
index be7597329e..932b602757 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
@@ -42,12 +42,14 @@ import 
org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
 import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.syncope.common.lib.to.AuthProfileTO;
 import org.apache.syncope.common.lib.types.AMEntitlement;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
 import org.apache.syncope.common.lib.wa.ImpersonationAccount;
 import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
 import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
@@ -66,14 +68,20 @@ public class AuthProfileDirectoryPanel
 
     private static final long serialVersionUID = 2018518567549153364L;
 
-    private String keyword;
+    private final ServiceOps serviceOps;
 
     private final BaseModal<AuthProfileTO> authProfileModal;
 
+    private String keyword;
+
     public AuthProfileDirectoryPanel(
-            final String id, final AuthProfileRestClient restClient, final 
PageReference pageRef) {
+            final String id,
+            final ServiceOps serviceOps,
+            final AuthProfileRestClient restClient,
+            final PageReference pageRef) {
 
         super(id, restClient, pageRef);
+        this.serviceOps = serviceOps;
 
         authProfileModal = new BaseModal<>(Constants.OUTER) {
 
@@ -162,6 +170,15 @@ public class AuthProfileDirectoryPanel
                 return 
CollectionUtils.isNotEmpty(rowModel.getObject().getWebAuthnDeviceCredentials());
             }
         });
+        columns.add(new BooleanConditionColumn<>(new 
StringResourceModel("consentDecisions")) {
+
+            private static final long serialVersionUID = -8236820422411536323L;
+
+            @Override
+            protected boolean isCondition(final IModel<AuthProfileTO> 
rowModel) {
+                return 
CollectionUtils.isNotEmpty(rowModel.getObject().getConsentDecisions());
+            }
+        });
 
         return columns;
     }
@@ -180,7 +197,7 @@ public class AuthProfileDirectoryPanel
                 target.add(authProfileModal.setContent(new 
ModalDirectoryPanel<>(
                         authProfileModal,
                         new 
AuthProfileItemDirectoryPanel<ImpersonationAccount>(
-                                "panel", restClient, authProfileModal, 
model.getObject(), pageRef) {
+                                "panel", serviceOps, restClient, 
authProfileModal, model.getObject(), null, pageRef) {
 
                     private static final long serialVersionUID = 
-5380664539000792237L;
 
@@ -227,7 +244,7 @@ public class AuthProfileDirectoryPanel
                 target.add(authProfileModal.setContent(new 
ModalDirectoryPanel<>(
                         authProfileModal,
                         new AuthProfileItemDirectoryPanel<GoogleMfaAuthToken>(
-                                "panel", restClient, authProfileModal, 
model.getObject(), pageRef) {
+                                "panel", serviceOps, restClient, 
authProfileModal, model.getObject(), null, pageRef) {
 
                     private static final long serialVersionUID = 
7332357430197837993L;
 
@@ -276,7 +293,7 @@ public class AuthProfileDirectoryPanel
                 target.add(authProfileModal.setContent(new 
ModalDirectoryPanel<>(
                         authProfileModal,
                         new 
AuthProfileItemDirectoryPanel<GoogleMfaAuthAccount>(
-                                "panel", restClient, authProfileModal, 
model.getObject(), pageRef) {
+                                "panel", serviceOps, restClient, 
authProfileModal, model.getObject(), null, pageRef) {
 
                     private static final long serialVersionUID = 
-670769282358547044L;
 
@@ -325,7 +342,7 @@ public class AuthProfileDirectoryPanel
                 target.add(authProfileModal.setContent(new 
ModalDirectoryPanel<>(
                         authProfileModal,
                         new AuthProfileItemDirectoryPanel<MfaTrustedDevice>(
-                                "panel", restClient, authProfileModal, 
model.getObject(), pageRef) {
+                                "panel", serviceOps, restClient, 
authProfileModal, model.getObject(), null, pageRef) {
 
                     private static final long serialVersionUID = 
5788448799796630011L;
 
@@ -376,7 +393,7 @@ public class AuthProfileDirectoryPanel
                 target.add(authProfileModal.setContent(new 
ModalDirectoryPanel<>(
                         authProfileModal,
                         new 
AuthProfileItemDirectoryPanel<WebAuthnDeviceCredential>(
-                                "panel", restClient, authProfileModal, 
model.getObject(), pageRef) {
+                                "panel", serviceOps, restClient, 
authProfileModal, model.getObject(), null, pageRef) {
 
                     private static final long serialVersionUID = 
6820212423488933184L;
 
@@ -415,6 +432,55 @@ public class AuthProfileDirectoryPanel
             }
         }, ActionLink.ActionType.HTML, AMEntitlement.AUTH_PROFILE_UPDATE);
 
+        panel.add(new ActionLink<>() {
+
+            private static final long serialVersionUID = -3722207913631435501L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final 
AuthProfileTO ignore) {
+                model.setObject(restClient.read(model.getObject().getKey()));
+                target.add(authProfileModal.setContent(new 
ModalDirectoryPanel<>(
+                        authProfileModal,
+                        new 
AuthProfileItemDirectoryPanel<WAConsentDecision>("panel", serviceOps,
+                                restClient, authProfileModal, 
model.getObject(), List.of("attributes"), pageRef) {
+
+                    private static final long serialVersionUID = 
-670769282358547044L;
+
+                    @Override
+                    protected List<WAConsentDecision> getItems() {
+                        return model.getObject().getConsentDecisions();
+                    }
+
+                    @Override
+                    protected WAConsentDecision defaultItem() {
+                        return new WAConsentDecision();
+                    }
+
+                    @Override
+                    protected String sortProperty() {
+                        return "id";
+                    }
+
+                    @Override
+                    protected String paginatorRowsKey() {
+                        return 
AMConstants.PREF_AUTHPROFILE_CONSENT_DECISION_PAGINATOR_ROWS;
+                    }
+
+                    @Override
+                    protected List<IColumn<WAConsentDecision, String>> 
getColumns() {
+                        List<IColumn<WAConsentDecision, String>> columns = new 
ArrayList<>();
+                        columns.add(new PropertyColumn<>(new 
ResourceModel("id"), "id", "id"));
+                        columns.add(new PropertyColumn<>(new 
ResourceModel("service"), "service", "service"));
+                        columns.add(new DatePropertyColumn<>(
+                                new ResourceModel("createdDate"), 
"createdDate", "createdDate"));
+                        return columns;
+                    }
+                }, pageRef)));
+                authProfileModal.header(new 
Model<>(getString("consentDecisions", model)));
+                authProfileModal.show(true);
+            }
+        }, ActionLink.ActionType.ASSIGN, AMEntitlement.AUTH_PROFILE_UPDATE);
+
         panel.add(new ActionLink<>() {
 
             private static final long serialVersionUID = -3722207913631435501L;
diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java
index cfa57a04ea..f795d7a483 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java
@@ -35,6 +35,7 @@ import 
org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
 import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
 import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.syncope.common.lib.BaseBean;
 import org.apache.syncope.common.lib.to.AuthProfileTO;
 import org.apache.syncope.common.lib.types.AMEntitlement;
@@ -60,9 +61,11 @@ public abstract class AuthProfileItemDirectoryPanel<I 
extends BaseBean>
 
     public AuthProfileItemDirectoryPanel(
             final String id,
+            final ServiceOps serviceOps,
             final AuthProfileRestClient restClient,
             final BaseModal<AuthProfileTO> authProfileModal,
             final AuthProfileTO authProfile,
+            final List<String> excluded,
             final PageReference pageRef) {
 
         super(id, restClient, pageRef, false);
@@ -75,6 +78,8 @@ public abstract class AuthProfileItemDirectoryPanel<I extends 
BaseBean>
         enableUtilityButton();
         setFooterVisibility(false);
 
+        addNewItemPanelBuilder(new AuthProfileItemWizardBuilder(excluded, 
serviceOps, restClient, pageRef), false);
+
         disableCheckBoxes();
         initResultTable();
     }
@@ -179,8 +184,13 @@ public abstract class AuthProfileItemDirectoryPanel<I 
extends BaseBean>
 
         private static final long serialVersionUID = -7174537333960225216L;
 
-        protected AuthProfileItemWizardBuilder(final PageReference pageRef) {
-            super(defaultItem(), new StepModel<>(), pageRef);
+        protected AuthProfileItemWizardBuilder(
+                final List<String> excluded,
+                final ServiceOps serviceOps,
+                final AuthProfileRestClient authProfileRestClient,
+                final PageReference pageRef) {
+
+            super(defaultItem(), new StepModel<>(), excluded, serviceOps, 
authProfileRestClient, pageRef);
         }
 
         @Override
diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfilePanel.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfilePanel.java
index b3f2607cfe..749f33f7e9 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfilePanel.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfilePanel.java
@@ -22,6 +22,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.console.commons.KeywordSearchEvent;
 import org.apache.syncope.client.console.rest.AuthProfileRestClient;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.markup.html.form.AjaxButton;
@@ -37,6 +38,7 @@ public class AuthProfilePanel extends Panel {
 
     public AuthProfilePanel(
             final String id,
+            final ServiceOps serviceOps,
             final AuthProfileRestClient authProfileRestClient,
             final PageReference pageRef) {
 
@@ -66,6 +68,7 @@ public class AuthProfilePanel extends Panel {
         form.add(search);
         form.setDefaultButton(search);
 
-        add(new AuthProfileDirectoryPanel("authProfiles", 
authProfileRestClient, pageRef).setOutputMarkupId(true));
+        add(new AuthProfileDirectoryPanel("authProfiles", serviceOps, 
authProfileRestClient, pageRef).
+                setOutputMarkupId(true));
     }
 }
diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder.java
index 41a5ff9958..44c53dc93e 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder.java
@@ -18,10 +18,16 @@
  */
 package org.apache.syncope.client.console.authprofiles;
 
+import java.util.List;
 import org.apache.commons.lang3.SerializationUtils;
 import org.apache.syncope.client.console.panels.BeanPanel;
+import org.apache.syncope.client.console.rest.AuthProfileRestClient;
+import 
org.apache.syncope.client.console.wicket.markup.html.form.JsonEditorPanel;
 import org.apache.syncope.client.console.wizards.BaseAjaxWizardBuilder;
+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.BaseBean;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.extensions.wizard.WizardModel;
 import org.apache.wicket.extensions.wizard.WizardStep;
@@ -33,14 +39,33 @@ public abstract class AuthProfileWizardBuilder<T extends 
BaseBean> extends BaseA
 
     protected final StepModel<T> model;
 
-    public AuthProfileWizardBuilder(final T defaultItem, final StepModel<T> 
model, final PageReference pageRef) {
+    protected final List<String> excluded;
+
+    protected final ServiceOps serviceOps;
+
+    protected final AuthProfileRestClient authProfileRestClient;
+
+    public AuthProfileWizardBuilder(
+            final T defaultItem,
+            final StepModel<T> model,
+            final List<String> excluded,
+            final ServiceOps serviceOps,
+            final AuthProfileRestClient authProfileRestClient,
+            final PageReference pageRef) {
+
         super(defaultItem, pageRef);
         this.model = model;
+        this.excluded = excluded;
+        this.serviceOps = serviceOps;
+        this.authProfileRestClient = authProfileRestClient;
     }
 
     @Override
     protected WizardModel buildModelSteps(final T modelObject, final 
WizardModel wizardModel) {
         wizardModel.add(new Step(modelObject));
+        if (modelObject instanceof WAConsentDecision consentDecision) {
+            wizardModel.add(new ConsentAttributes(consentDecision));
+        }
         return wizardModel;
     }
 
@@ -66,7 +91,32 @@ public abstract class AuthProfileWizardBuilder<T extends 
BaseBean> extends BaseA
         Step(final T modelObject) {
             model.setObject(modelObject);
             model.setInitialModelObject(modelObject);
-            add(new BeanPanel<>("bean", model, 
pageRef).setRenderBodyOnly(true));
+            add(new BeanPanel<>("bean", model, pageRef, excluded == null ? 
null : excluded.toArray(String[]::new)).
+                    setRenderBodyOnly(true));
+        }
+    }
+
+    protected class ConsentAttributes extends WizardStep {
+
+        private static final long serialVersionUID = -4865650799450548351L;
+
+        ConsentAttributes(final WAConsentDecision consentDecision) {
+            String attributes = "{}";
+            try {
+                attributes = authProfileRestClient.readConsentAttributes(
+                        serviceOps.get(NetworkService.Type.WA),
+                        consentDecision.getPrincipal(),
+                        consentDecision.getId());
+            } catch (Exception e) {
+                LOG.error("While attempting to fetch consent attributes for 
principal {} and id {}",
+                        consentDecision.getPrincipal(), 
consentDecision.getId(), e);
+            }
+            add(new JsonEditorPanel(null, Model.of(attributes), true, 
pageRef));
+        }
+
+        @Override
+        public String getTitle() {
+            return getString("attributes");
         }
     }
 }
diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java
index ae1e1347d9..2143e27046 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java
@@ -52,6 +52,9 @@ public final class AMConstants {
     public static final String 
PREF_AUTHPROFILE_WEBAUTHNDEVICECREDENTIALS_PAGINATOR_ROWS =
             "authprofile.webAuthnDeviceCredentials.paginator.rows";
 
+    public static final String 
PREF_AUTHPROFILE_CONSENT_DECISION_PAGINATOR_ROWS =
+            "authprofile.consentDecisions.paginator.rows";
+
     public static final String PREF_OIDC_CUSTOMSCOPES_PAGINATOR_ROWS = 
"oidc.customScopes.paginator.rows";
 
     private AMConstants() {
diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/pages/WA.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/pages/WA.java
index df2a52a8e9..b16398bc17 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/pages/WA.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/pages/WA.java
@@ -265,7 +265,7 @@ public class WA extends BasePage {
 
                 @Override
                 public Panel getPanel(final String panelId) {
-                    return new AuthProfilePanel(panelId, 
authProfileRestClient, getPageReference());
+                    return new AuthProfilePanel(panelId, serviceOps, 
authProfileRestClient, getPageReference());
                 }
             });
         }
diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/rest/AuthProfileRestClient.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/rest/AuthProfileRestClient.java
index 99f8cb18ce..6093a178a0 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/rest/AuthProfileRestClient.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/rest/AuthProfileRestClient.java
@@ -18,7 +18,17 @@
  */
 package org.apache.syncope.client.console.rest;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.List;
+import org.apache.commons.lang3.Strings;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.syncope.client.console.SyncopeWebApplication;
+import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
 import org.apache.syncope.common.lib.to.AuthProfileTO;
 import org.apache.syncope.common.rest.api.beans.AuthProfileQuery;
 import org.apache.syncope.common.rest.api.service.AuthProfileService;
@@ -27,6 +37,8 @@ public class AuthProfileRestClient extends BaseRestClient {
 
     private static final long serialVersionUID = -7379778542101161274L;
 
+    protected static final JsonMapper MAPPER = 
JsonMapper.builder().findAndAddModules().build();
+
     public long count(final String keyword) {
         return getService(AuthProfileService.class).
                 search(new 
AuthProfileQuery.Builder().page(1).size(0).keyword(keyword).build()).
@@ -50,4 +62,32 @@ public class AuthProfileRestClient extends BaseRestClient {
     public void delete(final String key) {
         getService(AuthProfileService.class).delete(key);
     }
+
+    public String readConsentAttributes(final NetworkService service, final 
String principal, final long id)
+            throws IOException {
+
+        Response response = WebClient.create(
+                Strings.CS.appendIfMissing(service.getAddress(), "/") + 
"actuator/attributeConsent/" + principal,
+                List.of(),
+                SyncopeWebApplication.get().getAnonymousUser(),
+                SyncopeWebApplication.get().getAnonymousKey(),
+                null).accept(MediaType.APPLICATION_JSON_TYPE).get();
+        if (response.getStatus() == Response.Status.OK.getStatusCode()) {
+            JsonNode nodes = MAPPER.readTree((InputStream) 
response.getEntity());
+            for (JsonNode node : nodes) {
+                if (node.has("decision")) {
+                    JsonNode decision = node.get("decision");
+                    if (decision.has("id") && id == 
decision.get("id").asLong()) {
+                        if (node.has("attributes")) {
+                            return node.get("attributes").toPrettyString();
+                        }
+                    }
+                }
+            }
+        } else {
+            LOG.error("While contacting the /actuator/attributeConsent 
endpoint: HTTP {}", response.getStatus());
+        }
+
+        return "{}";
+    }
 }
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties
index 196e06fde4..d933a844d8 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties
@@ -46,3 +46,9 @@ recordDate=Record Date
 mfaTrustedDevices=MFA Devices
 down.title=mfa devices
 down.class=fas fa-barcode
+consentDecisions=Consent Decisions
+service=Client Application
+createdDate=Created Date
+assign.title=consent decisions
+assign.class=fa-solid fa-clipboard-list
+attributes=Attributes
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties
index db0318cea8..a9e4e56144 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties
@@ -46,3 +46,9 @@ recordDate=Record Date
 mfaTrustedDevices=MFA Devices
 down.title=mfa devices
 down.class=fas fa-barcode
+consentDecisions=Consent Decisions
+service=Client Application
+createdDate=Created Date
+assign.title=consent decisions
+assign.class=fa-solid fa-clipboard-list
+attributes=Attributes
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties
index e9378093ce..555584bc7d 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties
@@ -46,3 +46,9 @@ recordDate=Memorizzazione
 mfaTrustedDevices=Dispositivi MFA
 down.title=dispositivi mfa
 down.class=fas fa-barcode
+consentDecisions=Consensi
+service=Applicazione client
+createdDate=Data creazione
+assign.title=consensi
+assign.class=fa-solid fa-clipboard-list
+attributes=Attributi
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties
index 4d3fd17e73..b0a43be7c2 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties
@@ -46,3 +46,9 @@ recordDate=Record Date
 mfaTrustedDevices=MFA Devices
 down.title=mfa devices
 down.class=fas fa-barcode
+consentDecisions=Consent Decisions
+service=Client Application
+createdDate=Created Date
+assign.title=consent decisions
+assign.class=fa-solid fa-clipboard-list
+attributes=Attributes
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties
index 9e1722ead7..fb3e964b47 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties
@@ -46,3 +46,9 @@ recordDate=Record Date
 mfaTrustedDevices=MFA Devices
 down.title=mfa devices
 down.class=fas fa-barcode
+consentDecisions=Consent Decisions
+service=Client Application
+createdDate=Created Date
+assign.title=consent decisions
+assign.class=fa-solid fa-clipboard-list
+attributes=Attributes
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties
index 028bda7565..9934e89a03 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties
@@ -47,3 +47,9 @@ recordDate=Record Date
 mfaTrustedDevices=MFA Devices
 down.title=mfa devices
 down.class=fas fa-barcode
+consentDecisions=Consent Decisions
+service=Client Application
+createdDate=Created Date
+assign.title=consent decisions
+assign.class=fa-solid fa-clipboard-list
+attributes=Attributes
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder$ConsentAttributes.html
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder$ConsentAttributes.html
new file mode 100644
index 0000000000..aefa83b98b
--- /dev/null
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder$ConsentAttributes.html
@@ -0,0 +1,23 @@
+<!--
+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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml"; 
xmlns:wicket="http://wicket.apache.org";>
+  <wicket:panel>
+    <div wicket:id="content"/>
+  </wicket:panel>
+</html>
diff --git 
a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AuthProfile.java
 
b/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AuthProfile.java
index 50a584d4f8..8f752e7fa3 100644
--- 
a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AuthProfile.java
+++ 
b/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AuthProfile.java
@@ -23,20 +23,26 @@ import java.util.stream.Collectors;
 import org.apache.syncope.client.enduser.rest.AuthProfileRestClient;
 import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.client.ui.commons.annotations.AMPage;
+import org.apache.syncope.client.ui.commons.markup.html.form.AlertBehavior;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.IndicatingOnConfirmAjaxLink;
+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.to.AuthProfileTO;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
 import org.apache.syncope.common.lib.wa.ImpersonationAccount;
 import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
 import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
 import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
 import 
org.apache.wicket.ajax.markup.html.navigation.paging.AjaxPagingNavigator;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.repeater.Item;
 import org.apache.wicket.markup.repeater.data.DataView;
 import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.model.Model;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.spring.injection.annot.SpringBean;
 
@@ -49,6 +55,9 @@ public class AuthProfile extends BaseReauthPage {
 
     protected static final int ROWS_PER_PAGE = 5;
 
+    @SpringBean
+    protected ServiceOps serviceOps;
+
     @SpringBean
     protected AuthProfileRestClient restClient;
 
@@ -216,5 +225,61 @@ public class AuthProfile extends BaseReauthPage {
         webAuthnDeviceCredentials.setItemsPerPage(ROWS_PER_PAGE);
         
container.add(webAuthnDeviceCredentials.setOutputMarkupPlaceholderTag(true));
         container.add(new 
AjaxPagingNavigator("webAuthnDeviceCredentialsNavigator", 
webAuthnDeviceCredentials));
+
+        DataView<WAConsentDecision> consentDecisions = new DataView<>(
+                "consentDecisions", new ListDataProvider<>(
+                        authProfile == null ? List.of() : 
authProfile.getConsentDecisions())) {
+
+            private static final long serialVersionUID = 6127875313385810666L;
+
+            @Override
+            public void populateItem(final Item<WAConsentDecision> item) {
+                String attributes = "{}";
+                try {
+                    attributes = restClient.readConsentAttributes(
+                            serviceOps.get(NetworkService.Type.WA),
+                            item.getModelObject().getPrincipal(),
+                            item.getModelObject().getId());
+                } catch (Exception e) {
+                    LOG.error("While attempting to fetch consent attributes 
for principal {} and id {}",
+                            item.getModelObject().getPrincipal(), 
item.getModelObject().getId(), e);
+                }
+
+                item.add(new Label("id", item.getModelObject().getId()));
+                item.add(new Label("service", 
item.getModelObject().getService()));
+                item.add(new Label("createdDate", 
item.getModelObject().getCreatedDate()));
+                AjaxLink<String> consentAttributes = new 
AjaxLink<>("consentAttributes", Model.of()) {
+
+                    private static final long serialVersionUID = 
2706290656177366584L;
+
+                    @Override
+                    public void onClick(final AjaxRequestTarget target) {
+                        // nothing to do
+                    }
+                };
+                item.add(consentAttributes.add(new AlertBehavior(
+                        consentAttributes,
+                        getString("attributes"),
+                        "<pre>' + JSON.stringify(JSON.parse('"
+                        + attributes.replace("'", "\'") + "'), null, 2) + 
'</pre>")));
+                item.add(new IndicatingOnConfirmAjaxLink<>(
+                        "consentDecisionDelete", Constants.CONFIRM_DELETE, 
true) {
+
+                    private static final long serialVersionUID = 
1632838687547839512L;
+
+                    @Override
+                    public void onClick(final AjaxRequestTarget target) {
+                        if (authProfile != null) {
+                            
authProfile.getConsentDecisions().remove(item.getModelObject());
+                            restClient.update(authProfile);
+                            target.add(container);
+                        }
+                    }
+                });
+            }
+        };
+        consentDecisions.setItemsPerPage(ROWS_PER_PAGE);
+        container.add(consentDecisions.setOutputMarkupPlaceholderTag(true));
+        container.add(new AjaxPagingNavigator("consentDecisionsNavigator", 
consentDecisions));
     }
 }
diff --git 
a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java
 
b/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java
index 2714954ff5..0f59438f1f 100644
--- 
a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java
+++ 
b/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java
@@ -18,6 +18,17 @@
  */
 package org.apache.syncope.client.enduser.rest;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import org.apache.commons.lang3.Strings;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
 import org.apache.syncope.common.lib.to.AuthProfileTO;
 import org.apache.syncope.common.rest.api.service.AuthProfileSelfService;
 
@@ -25,6 +36,8 @@ public class AuthProfileRestClient extends BaseRestClient {
 
     private static final long serialVersionUID = 4139153766778113329L;
 
+    protected static final JsonMapper MAPPER = 
JsonMapper.builder().findAndAddModules().build();
+
     public AuthProfileTO read() {
         try {
             return getService(AuthProfileSelfService.class).read();
@@ -41,4 +54,32 @@ public class AuthProfileRestClient extends BaseRestClient {
     public void delete() {
         getService(AuthProfileSelfService.class).delete();
     }
+
+    public String readConsentAttributes(final NetworkService service, final 
String principal, final long id)
+            throws IOException {
+
+        Response response = WebClient.create(
+                Strings.CS.appendIfMissing(service.getAddress(), "/") + 
"actuator/attributeConsent/" + principal,
+                List.of(),
+                SyncopeWebApplication.get().getAnonymousUser(),
+                SyncopeWebApplication.get().getAnonymousKey(),
+                null).accept(MediaType.APPLICATION_JSON_TYPE).get();
+        if (response.getStatus() == Response.Status.OK.getStatusCode()) {
+            JsonNode nodes = MAPPER.readTree((InputStream) 
response.getEntity());
+            for (JsonNode node : nodes) {
+                if (node.has("decision")) {
+                    JsonNode decision = node.get("decision");
+                    if (decision.has("id") && id == 
decision.get("id").asLong()) {
+                        if (node.has("attributes")) {
+                            return node.get("attributes").toString();
+                        }
+                    }
+                }
+            }
+        } else {
+            LOG.error("While contacting the /actuator/attributeConsent 
endpoint: HTTP {}", response.getStatus());
+        }
+
+        return "{}";
+    }
 }
diff --git 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.html
 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.html
index 17b7ea9a84..16b6801cca 100644
--- 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.html
+++ 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.html
@@ -208,7 +208,8 @@ under the License.
                     </div>
                   </div>
                 </div>
-              </div> <div class="box-body">
+              </div>
+              <div class="box-body">
                 <div class="box-header formcard">
                   <header class="card-container bg-danger">
                     <label class="form-label card-header-style">
@@ -247,7 +248,54 @@ under the License.
                     </div>
                   </div>
                 </div>
-              </div>   
+              </div>
+              <div class="box-body">
+                <div class="box-header formcard">
+                  <header class="card-container bg-danger">
+                    <label class="form-label card-header-style">
+                      <wicket:message key="consent.decisions.title"/>
+                    </label>
+                  </header>
+                  <div class="card-container-body">
+                    <div>
+                      <div class="col-xs-12">
+                        <div class="form-group">
+                          <table class="table table-striped table-bordered">
+                            <thead>
+                              <tr>
+                                <th><wicket:message key="id"/></th>
+                                <th><wicket:message key="service"/></th>
+                                <th><wicket:message key="createdDate"/></th>
+                                <th></th>
+                              </tr>
+                            </thead>
+                            <tbody>
+                              <tr wicket:id="consentDecisions">
+                                <td><span wicket:id="id"/></td>
+                                <td><span wicket:id="service"/></td>
+                                <td><span wicket:id="createdDate"/></td>
+                                <td style="width: 20px;">
+                                  <a href="#" wicket:id="consentAttributes"><i 
class="fas fa-search-plus"></i></a>
+                                </td>                                
+                                <td style="width: 20px;">
+                                  <a href="#" 
wicket:id="consentDecisionDelete"><i class="fas fa-trash"></i></a>
+                                </td>                                
+                              </tr>
+                            </tbody>
+                          </table>
+                          <table class="paginator">
+                            <tfoot>
+                              <tr>
+                                <td wicket:id="consentDecisionsNavigator"></td>
+                              </tr>
+                            </tfoot>
+                          </table>                          
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </div>              
             </div>
           </div>
         </div>
diff --git 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.properties
 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.properties
index 5bf4119b60..ec839853d2 100644
--- 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.properties
+++ 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.properties
@@ -33,3 +33,7 @@ source=Source
 deviceFingerprint=Fingerprint
 recordDate=Record Date
 expirationDate=Expiration Date
+consent.decisions.title=Consent Decisions
+service=Application
+createdDate=Created Date
+attributes=Attributes
diff --git 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_it.properties
 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_it.properties
index bb47fe6606..c1dca5b588 100644
--- 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_it.properties
+++ 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_it.properties
@@ -33,3 +33,7 @@ source=Origine
 deviceFingerprint=Fingerprint
 recordDate=Data di registrazione
 expirationDate=Data di scadenza
+consent.decisions.title=Consensi
+service=Applicazione
+createdDate=Data creazione
+attributes=Attributi
diff --git 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ja.properties
 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ja.properties
index 5bf4119b60..ec839853d2 100644
--- 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ja.properties
+++ 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ja.properties
@@ -33,3 +33,7 @@ source=Source
 deviceFingerprint=Fingerprint
 recordDate=Record Date
 expirationDate=Expiration Date
+consent.decisions.title=Consent Decisions
+service=Application
+createdDate=Created Date
+attributes=Attributes
diff --git 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_pt_BR.properties
 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_pt_BR.properties
index 5bf4119b60..ec839853d2 100644
--- 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_pt_BR.properties
+++ 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_pt_BR.properties
@@ -33,3 +33,7 @@ source=Source
 deviceFingerprint=Fingerprint
 recordDate=Record Date
 expirationDate=Expiration Date
+consent.decisions.title=Consent Decisions
+service=Application
+createdDate=Created Date
+attributes=Attributes
diff --git 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ru.properties
 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ru.properties
index 5bf4119b60..ec839853d2 100644
--- 
a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ru.properties
+++ 
b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ru.properties
@@ -33,3 +33,7 @@ source=Source
 deviceFingerprint=Fingerprint
 recordDate=Record Date
 expirationDate=Expiration Date
+consent.decisions.title=Consent Decisions
+service=Application
+createdDate=Created Date
+attributes=Attributes
diff --git 
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AlertBehavior.java
 
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AlertBehavior.java
new file mode 100644
index 0000000000..16f29c7ecc
--- /dev/null
+++ 
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AlertBehavior.java
@@ -0,0 +1,68 @@
+/*
+ * 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.client.ui.commons.markup.html.form;
+
+import static de.agilecoders.wicket.jquery.JQuery.$;
+
+import de.agilecoders.wicket.jquery.function.JavaScriptInlineFunction;
+import java.util.ArrayList;
+import org.apache.wicket.Component;
+import org.apache.wicket.Session;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.markup.head.IHeaderResponse;
+
+public class AlertBehavior extends Behavior {
+
+    private static final long serialVersionUID = 2210125898183667592L;
+
+    private final Component parent;
+
+    private final String title;
+
+    private final String body;
+
+    public AlertBehavior(final Component parent, final String title, final 
String body) {
+        this.parent = parent;
+        this.title = title;
+        this.body = body;
+    }
+
+    @Override
+    public void renderHead(final Component component, final IHeaderResponse 
response) {
+        super.renderHead(component, response);
+
+        response.render($(parent).on("click",
+                new JavaScriptInlineFunction(""
+                        + "bootbox.alert({"
+                        + "size:'large', "
+                        + "title:'" + title + "', "
+                        + "message: '" + body + "', "
+                        + "buttons: {"
+                        + "    ok: {"
+                        + "        className: 'btn-success'"
+                        + "    }"
+                        + "},"
+                        + "locale: '" + 
Session.get().getLocale().getLanguage() + "',"
+                        + "callback: function() {"
+                        + "  return true;"
+                        + "}"
+                        + "});", new ArrayList<>()
+                )).asDomReadyScript());
+    }
+}
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/JsonEditorPanel.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/JsonEditorPanel.java
index 7efb694d90..460ed128aa 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/JsonEditorPanel.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/JsonEditorPanel.java
@@ -34,10 +34,6 @@ public class JsonEditorPanel extends 
AbstractModalPanel<String> {
 
     private final boolean readOnly;
 
-    public JsonEditorPanel(final IModel<String> content) {
-        this(null, content, false, null);
-    }
-
     public JsonEditorPanel(
             final BaseModal<String> modal,
             final IModel<String> content,
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
index 92c06999a4..2063e2ed71 100644
--- 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.common.lib.to;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import jakarta.ws.rs.PathParam;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -28,9 +29,10 @@ import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
 import org.apache.syncope.common.lib.wa.ImpersonationAccount;
 import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
 import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
 
-public class AuthProfileTO implements EntityTO {
+public class AuthProfileTO implements NamedEntityTO {
 
     private static final long serialVersionUID = -6543425997956703057L;
 
@@ -48,6 +50,23 @@ public class AuthProfileTO implements EntityTO {
             return this;
         }
 
+        public AuthProfileTO.Builder impersonationAccount(final 
ImpersonationAccount impersonationAccount) {
+            instance.getImpersonationAccounts().add(impersonationAccount);
+            return this;
+        }
+
+        public AuthProfileTO.Builder impersonationAccounts(final 
ImpersonationAccount... impersonationAccounts) {
+            
instance.getImpersonationAccounts().addAll(List.of(impersonationAccounts));
+            return this;
+        }
+
+        public AuthProfileTO.Builder impersonationAccounts(
+                final Collection<ImpersonationAccount> impersonationAccounts) {
+
+            instance.getImpersonationAccounts().addAll(impersonationAccounts);
+            return this;
+        }
+
         public AuthProfileTO.Builder googleMfaAuthToken(final 
GoogleMfaAuthToken token) {
             instance.getGoogleMfaAuthTokens().add(token);
             return this;
@@ -93,21 +112,36 @@ public class AuthProfileTO implements EntityTO {
             return this;
         }
 
-        public AuthProfileTO.Builder credential(final WebAuthnDeviceCredential 
credential) {
+        public AuthProfileTO.Builder webAuthnDeviceCredential(final 
WebAuthnDeviceCredential credential) {
             instance.getWebAuthnDeviceCredentials().add(credential);
             return this;
         }
 
-        public AuthProfileTO.Builder credentials(final 
WebAuthnDeviceCredential... credentials) {
+        public AuthProfileTO.Builder webAuthnDeviceCredentials(final 
WebAuthnDeviceCredential... credentials) {
             
instance.getWebAuthnDeviceCredentials().addAll(List.of(credentials));
             return this;
         }
 
-        public AuthProfileTO.Builder credentials(final 
Collection<WebAuthnDeviceCredential> credentials) {
+        public AuthProfileTO.Builder webAuthnDeviceCredentials(final 
Collection<WebAuthnDeviceCredential> credentials) {
             instance.getWebAuthnDeviceCredentials().addAll(credentials);
             return this;
         }
 
+        public AuthProfileTO.Builder consentDecision(final WAConsentDecision 
consentDecision) {
+            instance.getConsentDecisions().add(consentDecision);
+            return this;
+        }
+
+        public AuthProfileTO.Builder consentDecisions(final 
WAConsentDecision... consentDecisions) {
+            instance.getConsentDecisions().addAll(List.of(consentDecisions));
+            return this;
+        }
+
+        public AuthProfileTO.Builder consentDecisions(final 
Collection<WAConsentDecision> consentDecisions) {
+            instance.getConsentDecisions().addAll(consentDecisions);
+            return this;
+        }
+
         public AuthProfileTO build() {
             return instance;
         }
@@ -127,6 +161,8 @@ public class AuthProfileTO implements EntityTO {
 
     private final List<WebAuthnDeviceCredential> webAuthnDeviceCredentials = 
new ArrayList<>();
 
+    private final List<WAConsentDecision> consentDecisions = new ArrayList<>();
+
     @Override
     public String getKey() {
         return key;
@@ -146,6 +182,18 @@ public class AuthProfileTO implements EntityTO {
         this.owner = owner;
     }
 
+    @JsonIgnore
+    @Override
+    public String getName() {
+        return getOwner();
+    }
+
+    @JsonIgnore
+    @Override
+    public void setName(final String name) {
+        throw new UnsupportedOperationException();
+    }
+
     public List<ImpersonationAccount> getImpersonationAccounts() {
         return impersonationAccounts;
     }
@@ -166,6 +214,10 @@ public class AuthProfileTO implements EntityTO {
         return webAuthnDeviceCredentials;
     }
 
+    public List<WAConsentDecision> getConsentDecisions() {
+        return consentDecisions;
+    }
+
     @Override
     public int hashCode() {
         return new HashCodeBuilder().
@@ -176,6 +228,7 @@ public class AuthProfileTO implements EntityTO {
                 append(googleMfaAuthAccounts).
                 append(mfaTrustedDevices).
                 append(webAuthnDeviceCredentials).
+                append(consentDecisions).
                 build();
     }
 
@@ -199,6 +252,7 @@ public class AuthProfileTO implements EntityTO {
                 append(googleMfaAuthAccounts, other.googleMfaAuthAccounts).
                 append(mfaTrustedDevices, other.mfaTrustedDevices).
                 append(webAuthnDeviceCredentials, 
other.webAuthnDeviceCredentials).
+                append(consentDecisions, other.consentDecisions).
                 build();
     }
 }
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/WAConsentDecision.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/WAConsentDecision.java
new file mode 100644
index 0000000000..6f33c07b14
--- /dev/null
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/WAConsentDecision.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.lib.wa;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.syncope.common.lib.BaseBean;
+
+public class WAConsentDecision implements BaseBean {
+
+    private static final long serialVersionUID = -4763224069061622840L;
+
+    public static class Builder {
+
+        private final WAConsentDecision instance;
+
+        public Builder(final long id, final String principal, final String 
service, final LocalDateTime createDate) {
+            instance = new WAConsentDecision();
+            instance.setId(id);
+            instance.setPrincipal(principal);
+            instance.setService(service);
+            instance.setCreatedDate(createDate);
+        }
+
+        public Builder options(final ReminderOptions options) {
+            instance.setOptions(options);
+            return this;
+        }
+
+        public Builder reminder(final long reminder) {
+            instance.setReminder(reminder);
+            return this;
+        }
+
+        public Builder reminderTimeUnit(final ChronoUnit reminderTimeUnit) {
+            instance.setReminderTimeUnit(reminderTimeUnit);
+            return this;
+        }
+
+        public Builder attributes(final String attributes) {
+            instance.setAttributes(attributes);
+            return this;
+        }
+
+        public WAConsentDecision build() {
+            return instance;
+        }
+    }
+
+    public enum ReminderOptions {
+        /**
+         * Always ask for consent.
+         */
+        ALWAYS(0),
+        /**
+         * Ask for consent when there is modification in one of the attribute 
names or if consent is expired.
+         */
+        ATTRIBUTE_NAME(1),
+        /**
+         * Ask for consent when there is modification in one of the attribute 
names, the values contain inside the
+         * attributes or if consent is expired.
+         */
+        ATTRIBUTE_VALUE(2);
+
+        private final int value;
+
+        ReminderOptions(final int value) {
+            this.value = value;
+        }
+
+        public int getValue() {
+            return value;
+        }
+    }
+
+    private long id;
+
+    private String principal;
+
+    private String service;
+
+    private LocalDateTime createdDate;
+
+    private ReminderOptions options = ReminderOptions.ATTRIBUTE_NAME;
+
+    private long reminder = 14L;
+
+    private ChronoUnit reminderTimeUnit = ChronoUnit.DAYS;
+
+    private String attributes;
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(final long id) {
+        this.id = id;
+    }
+
+    public String getPrincipal() {
+        return principal;
+    }
+
+    public void setPrincipal(final String principal) {
+        this.principal = principal;
+    }
+
+    public String getService() {
+        return service;
+    }
+
+    public void setService(final String service) {
+        this.service = service;
+    }
+
+    public LocalDateTime getCreatedDate() {
+        return createdDate;
+    }
+
+    public void setCreatedDate(final LocalDateTime createdDate) {
+        this.createdDate = createdDate;
+    }
+
+    public ReminderOptions getOptions() {
+        return options;
+    }
+
+    public void setOptions(final ReminderOptions options) {
+        this.options = options;
+    }
+
+    public long getReminder() {
+        return reminder;
+    }
+
+    public void setReminder(final long reminder) {
+        this.reminder = reminder;
+    }
+
+    public ChronoUnit getReminderTimeUnit() {
+        return reminderTimeUnit;
+    }
+
+    public void setReminderTimeUnit(final ChronoUnit reminderTimeUnit) {
+        this.reminderTimeUnit = reminderTimeUnit;
+    }
+
+    public String getAttributes() {
+        return attributes;
+    }
+
+    public void setAttributes(final String attributes) {
+        this.attributes = attributes;
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                append(id).
+                append(principal).
+                append(service).
+                append(createdDate).
+                append(options).
+                append(reminder).
+                append(reminderTimeUnit).
+                append(attributes).
+                build();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        WAConsentDecision other = (WAConsentDecision) obj;
+        return new EqualsBuilder().
+                append(id, other.id).
+                append(principal, other.principal).
+                append(service, other.service).
+                append(createdDate, other.createdDate).
+                append(options, other.options).
+                append(reminder, other.reminder).
+                append(reminderTimeUnit, other.reminderTimeUnit).
+                append(attributes, other.attributes).
+                build();
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this).
+                append("id", id).
+                append("principal", principal).
+                append("service", service).
+                append("createdDate", createdDate).
+                append("options", options).
+                append("reminder", reminder).
+                append("reminderTimeUnit", reminderTimeUnit).
+                append("attributes", attributes).
+                build();
+    }
+}
diff --git 
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/ConsentDecisionService.java
 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/ConsentDecisionService.java
new file mode 100644
index 0000000000..96c3b45716
--- /dev/null
+++ 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/ConsentDecisionService.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.rest.api.service.wa;
+
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.security.SecurityRequirements;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.constraints.NotNull;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
+
+@Tag(name = "WA")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer") })
+@Path("wa/consentDecision")
+public interface ConsentDecisionService extends JAXRSService {
+
+    @DELETE
+    @Path("{owner}/{id}")
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    void delete(@NotNull @PathParam("owner") String owner, @NotNull 
@PathParam("id") long id);
+
+    @DELETE
+    @Path("{owner}")
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    void delete(@NotNull @PathParam("owner") String owner);
+
+    @DELETE
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    void deleteAll();
+
+    @PUT
+    @Path("{owner}")
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    void store(@NotNull @PathParam("owner") String owner, @NotNull 
WAConsentDecision consentDecision);
+
+    @GET
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Path("{owner}/service")
+    WAConsentDecision read(@NotNull @PathParam("owner") String owner, @NotNull 
@QueryParam("service") String service);
+
+    @GET
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Path("{owner}")
+    PagedResult<WAConsentDecision> read(@NotNull @PathParam("owner") String 
owner);
+
+    @GET
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    PagedResult<WAConsentDecision> list();
+}
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java
index 7cbf075742..f14808b37a 100644
--- 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java
@@ -20,6 +20,7 @@ package org.apache.syncope.core.logic;
 
 import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.syncope.core.logic.init.AMEntitlementLoader;
+import org.apache.syncope.core.logic.wa.ConsentDecisionLogic;
 import org.apache.syncope.core.logic.wa.GoogleMfaAuthAccountLogic;
 import org.apache.syncope.core.logic.wa.GoogleMfaAuthTokenLogic;
 import org.apache.syncope.core.logic.wa.ImpersonationLogic;
@@ -181,6 +182,16 @@ public class AMLogicContext {
         return new ImpersonationLogic(authProfileDataBinder, authProfileDAO, 
entityFactory);
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public ConsentDecisionLogic consentDecisionLogic(
+            final AuthProfileDataBinder authProfileDataBinder,
+            final AuthProfileDAO authProfileDAO,
+            final EntityFactory entityFactory) {
+
+        return new ConsentDecisionLogic(authProfileDataBinder, authProfileDAO, 
entityFactory);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public MfaTrusStorageLogic mfaTrusStorageLogic(
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ConsentDecisionLogic.java
 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ConsentDecisionLogic.java
new file mode 100644
index 0000000000..082c1a17c7
--- /dev/null
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ConsentDecisionLogic.java
@@ -0,0 +1,112 @@
+/*
+ * 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.wa;
+
+import java.util.List;
+import java.util.function.Predicate;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
+import org.apache.syncope.core.logic.AbstractAuthProfileLogic;
+import org.apache.syncope.core.persistence.api.dao.AuthProfileDAO;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.am.AuthProfile;
+import org.apache.syncope.core.provisioning.api.data.AuthProfileDataBinder;
+import org.springframework.data.domain.Pageable;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.transaction.annotation.Transactional;
+
+public class ConsentDecisionLogic extends AbstractAuthProfileLogic {
+
+    public ConsentDecisionLogic(
+            final AuthProfileDataBinder binder,
+            final AuthProfileDAO authProfileDAO,
+            final EntityFactory entityFactory) {
+
+        super(binder, authProfileDAO, entityFactory);
+    }
+
+    protected void removeAndSave(final AuthProfile profile, final 
Predicate<WAConsentDecision> criteria) {
+        if (profile.getConsentDecisions().removeIf(criteria)) {
+            authProfileDAO.save(profile);
+        }
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public void delete(final String owner, final long id) {
+        authProfileDAO.findByOwner(owner).
+                ifPresent(profile -> removeAndSave(profile, consentDecision -> 
consentDecision.getId() == id));
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public void delete(final String owner) {
+        authProfileDAO.findByOwner(owner).ifPresent(profile -> {
+            profile.getConsentDecisions().clear();
+            authProfileDAO.save(profile);
+        });
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public void deleteAll() {
+        authProfileDAO.findAll(Pageable.unpaged()).forEach(profile -> {
+            profile.getConsentDecisions().clear();
+            authProfileDAO.save(profile);
+        });
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public void store(final String owner, final WAConsentDecision 
contentDecision) {
+        AuthProfile profile = authProfile(owner);
+
+        profile.getConsentDecisions().removeIf(cd -> cd.getId() == 
contentDecision.getId());
+        profile.add(contentDecision);
+
+        authProfileDAO.save(profile);
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public WAConsentDecision read(final String owner, final String service) {
+        return authProfileDAO.findByOwner(owner).
+                stream().
+                map(AuthProfile::getConsentDecisions).
+                flatMap(List::stream).
+                filter(consentDecision -> 
consentDecision.getService().equals(service)).
+                findFirst().
+                orElseThrow(() -> new NotFoundException(
+                "Could not find consent decision for owner " + owner + " and 
service " + service));
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public List<WAConsentDecision> list() {
+        return authProfileDAO.findAll(Pageable.unpaged()).stream().
+                map(AuthProfile::getConsentDecisions).
+                flatMap(List::stream).
+                toList();
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public List<WAConsentDecision> read(final String owner) {
+        return authProfileDAO.findByOwner(owner).
+                map(AuthProfile::getConsentDecisions).
+                orElseGet(List::of);
+    }
+}
diff --git 
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java
 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java
index 157f836306..99fa06d712 100644
--- 
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java
+++ 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java
@@ -27,6 +27,7 @@ import 
org.apache.syncope.common.rest.api.service.OIDCOpEntityService;
 import org.apache.syncope.common.rest.api.service.PasswordManagementService;
 import org.apache.syncope.common.rest.api.service.SAML2IdPEntityService;
 import org.apache.syncope.common.rest.api.service.SRARouteService;
+import org.apache.syncope.common.rest.api.service.wa.ConsentDecisionService;
 import 
org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthAccountService;
 import org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthTokenService;
 import org.apache.syncope.common.rest.api.service.wa.ImpersonationService;
@@ -43,6 +44,7 @@ import org.apache.syncope.core.logic.OIDCOpEntityLogic;
 import org.apache.syncope.core.logic.PasswordManagementLogic;
 import org.apache.syncope.core.logic.SAML2IdPEntityLogic;
 import org.apache.syncope.core.logic.SRARouteLogic;
+import org.apache.syncope.core.logic.wa.ConsentDecisionLogic;
 import org.apache.syncope.core.logic.wa.GoogleMfaAuthAccountLogic;
 import org.apache.syncope.core.logic.wa.GoogleMfaAuthTokenLogic;
 import org.apache.syncope.core.logic.wa.ImpersonationLogic;
@@ -59,6 +61,7 @@ import 
org.apache.syncope.core.rest.cxf.service.OIDCOpEntityServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.PasswordManagementServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.SAML2IdPEntityServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.SRARouteServiceImpl;
+import org.apache.syncope.core.rest.cxf.service.wa.ConsentDecisionServiceImpl;
 import 
org.apache.syncope.core.rest.cxf.service.wa.GoogleMfaAuthAccountServiceImpl;
 import 
org.apache.syncope.core.rest.cxf.service.wa.GoogleMfaAuthTokenServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.wa.ImpersonationServiceImpl;
@@ -132,6 +135,14 @@ public class AMRESTCXFContext {
         return new ImpersonationServiceImpl(impersonationLogic);
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public ConsentDecisionService consentDecisionService(
+            final ConsentDecisionLogic consentDecisionLogic) {
+
+        return new ConsentDecisionServiceImpl(consentDecisionLogic);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public OIDCOpEntityService oidcOpService(final OIDCOpEntityLogic 
oidcOpLogic) {
diff --git 
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/ConsentDecisionServiceImpl.java
 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/ConsentDecisionServiceImpl.java
new file mode 100644
index 0000000000..3ecffb5b39
--- /dev/null
+++ 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/ConsentDecisionServiceImpl.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.rest.cxf.service.wa;
+
+import java.util.List;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
+import org.apache.syncope.common.rest.api.service.wa.ConsentDecisionService;
+import org.apache.syncope.core.logic.wa.ConsentDecisionLogic;
+import org.apache.syncope.core.rest.cxf.service.AbstractService;
+
+public class ConsentDecisionServiceImpl extends AbstractService implements 
ConsentDecisionService {
+
+    protected final ConsentDecisionLogic logic;
+
+    public ConsentDecisionServiceImpl(final ConsentDecisionLogic logic) {
+        this.logic = logic;
+    }
+
+    @Override
+    public void delete(final String owner, final long id) {
+        logic.delete(owner, id);
+    }
+
+    @Override
+    public void delete(final String owner) {
+        logic.delete(owner);
+    }
+
+    @Override
+    public void deleteAll() {
+        logic.deleteAll();
+    }
+
+    @Override
+    public void store(final String owner, final WAConsentDecision 
consentDecision) {
+        logic.store(owner, consentDecision);
+    }
+
+    @Override
+    public WAConsentDecision read(final String owner, final String service) {
+        return logic.read(owner, service);
+    }
+
+    private PagedResult<WAConsentDecision> build(final List<WAConsentDecision> 
read) {
+        PagedResult<WAConsentDecision> result = new PagedResult<>();
+        result.setPage(1);
+        result.setSize(read.size());
+        result.setTotalCount(read.size());
+        result.getResult().addAll(read);
+        return result;
+    }
+
+    @Override
+    public PagedResult<WAConsentDecision> read(final String owner) {
+        return build(logic.read(owner));
+    }
+
+    @Override
+    public PagedResult<WAConsentDecision> list() {
+        return build(logic.list());
+    }
+}
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java
index f79e3ab8ea..c19eed5e7e 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java
@@ -23,6 +23,7 @@ import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
 import org.apache.syncope.common.lib.wa.ImpersonationAccount;
 import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
 import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
 import org.apache.syncope.core.persistence.api.entity.Entity;
 
@@ -51,4 +52,8 @@ public interface AuthProfile extends Entity {
     boolean add(ImpersonationAccount impersonationAccount);
 
     List<ImpersonationAccount> getImpersonationAccounts();
+
+    boolean add(WAConsentDecision consentDecision);
+
+    List<WAConsentDecision> getConsentDecisions();
 }
diff --git 
a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/converters/WAConsentDecisionListConverter.java
similarity index 50%
copy from 
client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java
copy to 
core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/converters/WAConsentDecisionListConverter.java
index 2714954ff5..f13aad72a3 100644
--- 
a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/converters/WAConsentDecisionListConverter.java
@@ -16,29 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.rest;
+package org.apache.syncope.core.persistence.jpa.converters;
 
-import org.apache.syncope.common.lib.to.AuthProfileTO;
-import org.apache.syncope.common.rest.api.service.AuthProfileSelfService;
+import jakarta.persistence.Converter;
+import java.util.List;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
+import tools.jackson.core.type.TypeReference;
 
-public class AuthProfileRestClient extends BaseRestClient {
+@Converter
+public class WAConsentDecisionListConverter extends 
SerializableListConverter<WAConsentDecision> {
 
-    private static final long serialVersionUID = 4139153766778113329L;
+    protected static final TypeReference<List<WAConsentDecision>> TYPEREF =
+            new TypeReference<List<WAConsentDecision>>() {
+    };
 
-    public AuthProfileTO read() {
-        try {
-            return getService(AuthProfileSelfService.class).read();
-        } catch (Exception e) {
-            LOG.debug("While attempting to read the auth profile", e);
-            return null;
-        }
-    }
-
-    public void update(final AuthProfileTO authProfile) {
-        getService(AuthProfileSelfService.class).update(authProfile);
-    }
-
-    public void delete() {
-        getService(AuthProfileSelfService.class).delete();
+    @Override
+    protected TypeReference<List<WAConsentDecision>> typeRef() {
+        return TYPEREF;
     }
 }
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
index 993b485f22..b869723272 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
@@ -30,12 +30,14 @@ import 
org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
 import org.apache.syncope.common.lib.wa.ImpersonationAccount;
 import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
 import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
 import org.apache.syncope.core.persistence.api.entity.am.AuthProfile;
 import 
org.apache.syncope.core.persistence.jpa.converters.GoogleMfaAuthAccountListConverter;
 import 
org.apache.syncope.core.persistence.jpa.converters.GoogleMfaAuthTokenListConverter;
 import 
org.apache.syncope.core.persistence.jpa.converters.ImpersonationAccountListConverter;
 import 
org.apache.syncope.core.persistence.jpa.converters.MfaTrustedDeviceListConverter;
+import 
org.apache.syncope.core.persistence.jpa.converters.WAConsentDecisionListConverter;
 import 
org.apache.syncope.core.persistence.jpa.converters.WebAuthnDeviceCredentialListConverter;
 import 
org.apache.syncope.core.persistence.jpa.entity.AbstractGeneratedKeyEntity;
 
@@ -71,6 +73,10 @@ public class JPAAuthProfile extends 
AbstractGeneratedKeyEntity implements AuthPr
     @Lob
     private List<WebAuthnDeviceCredential> webAuthnDeviceCredentials = new 
ArrayList<>();
 
+    @Convert(converter = WAConsentDecisionListConverter.class)
+    @Lob
+    private List<WAConsentDecision> waConsentDecisions = new ArrayList<>();
+
     @Override
     public String getOwner() {
         return owner;
@@ -135,4 +141,15 @@ public class JPAAuthProfile extends 
AbstractGeneratedKeyEntity implements AuthPr
     public List<WebAuthnDeviceCredential> getWebAuthnDeviceCredentials() {
         return webAuthnDeviceCredentials;
     }
+
+    @Override
+    public boolean add(final WAConsentDecision consentDecision) {
+        return !waConsentDecisions.contains(consentDecision)
+                && waConsentDecisions.add(consentDecision);
+    }
+
+    @Override
+    public List<WAConsentDecision> getConsentDecisions() {
+        return waConsentDecisions;
+    }
 }
diff --git 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/am/Neo4jAuthProfile.java
 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/am/Neo4jAuthProfile.java
index 8e85783728..3acc813781 100644
--- 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/am/Neo4jAuthProfile.java
+++ 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/am/Neo4jAuthProfile.java
@@ -26,6 +26,7 @@ import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount;
 import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
 import org.apache.syncope.common.lib.wa.ImpersonationAccount;
 import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
 import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
 import org.apache.syncope.core.persistence.api.entity.am.AuthProfile;
 import 
org.apache.syncope.core.persistence.neo4j.entity.AbstractGeneratedKeyNode;
@@ -62,6 +63,10 @@ public class Neo4jAuthProfile extends 
AbstractGeneratedKeyNode implements AuthPr
             new TypeReference<List<WebAuthnDeviceCredential>>() {
     };
 
+    protected static final TypeReference<List<WAConsentDecision>> 
WA_CONSENT_DECISION_TYPEREF =
+            new TypeReference<List<WAConsentDecision>>() {
+    };
+
     @NotNull
     private String owner;
 
@@ -90,6 +95,11 @@ public class Neo4jAuthProfile extends 
AbstractGeneratedKeyNode implements AuthPr
     @Transient
     private List<WebAuthnDeviceCredential> webAuthnDeviceCredentialsList = new 
ArrayList<>();
 
+    private String waConsentDecisions;
+
+    @Transient
+    private List<WAConsentDecision> waConsentDecisionsList = new ArrayList<>();
+
     @Override
     public String getOwner() {
         return owner;
@@ -150,6 +160,16 @@ public class Neo4jAuthProfile extends 
AbstractGeneratedKeyNode implements AuthPr
         return webAuthnDeviceCredentialsList;
     }
 
+    @Override
+    public boolean add(final WAConsentDecision consentDecision) {
+        return waConsentDecisionsList.contains(consentDecision);
+    }
+
+    @Override
+    public List<WAConsentDecision> getConsentDecisions() {
+        return waConsentDecisionsList;
+    }
+
     protected void json2list(final boolean clearFirst) {
         if (clearFirst) {
             getGoogleMfaAuthTokens().clear();
@@ -157,6 +177,7 @@ public class Neo4jAuthProfile extends 
AbstractGeneratedKeyNode implements AuthPr
             getMfaTrustedDevices().clear();
             getImpersonationAccounts().clear();
             getWebAuthnDeviceCredentials().clear();
+            getConsentDecisions().clear();
         }
         Optional.ofNullable(googleMfaAuthTokens).ifPresent(v -> 
getGoogleMfaAuthTokens().
                 addAll(POJOHelper.deserialize(v, GOOGLE_MFA_TOKENS_TYPEREF)));
@@ -168,6 +189,8 @@ public class Neo4jAuthProfile extends 
AbstractGeneratedKeyNode implements AuthPr
                 addAll(POJOHelper.deserialize(v, IMPERSONATION_TYPEREF)));
         Optional.ofNullable(webAuthnDeviceCredentials).ifPresent(v -> 
getWebAuthnDeviceCredentials().
                 addAll(POJOHelper.deserialize(v, WEBAUTHN_TYPEREF)));
+        Optional.ofNullable(waConsentDecisions).ifPresent(v -> 
getConsentDecisions().
+                addAll(POJOHelper.deserialize(v, 
WA_CONSENT_DECISION_TYPEREF)));
     }
 
     @PostLoad
@@ -185,5 +208,6 @@ public class Neo4jAuthProfile extends 
AbstractGeneratedKeyNode implements AuthPr
         mfaTrustedDevices = POJOHelper.serialize(getMfaTrustedDevices());
         impersonationAccounts = 
POJOHelper.serialize(getImpersonationAccounts());
         webAuthnDeviceCredentials = 
POJOHelper.serialize(getWebAuthnDeviceCredentials());
+        waConsentDecisions = POJOHelper.serialize(getConsentDecisions());
     }
 }
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
index 38cf317e30..00b7b48036 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java
@@ -33,15 +33,16 @@ public class AuthProfileDataBinderImpl implements 
AuthProfileDataBinder {
 
     @Override
     public AuthProfileTO getAuthProfileTO(final AuthProfile authProfile) {
-        AuthProfileTO authProfileTO = new AuthProfileTO();
-        authProfileTO.setKey(authProfile.getKey());
-        authProfileTO.setOwner(authProfile.getOwner());
-        
authProfileTO.getImpersonationAccounts().addAll(authProfile.getImpersonationAccounts());
-        
authProfileTO.getGoogleMfaAuthTokens().addAll(authProfile.getGoogleMfaAuthTokens());
-        
authProfileTO.getGoogleMfaAuthAccounts().addAll(authProfile.getGoogleMfaAuthAccounts());
-        
authProfileTO.getMfaTrustedDevices().addAll(authProfile.getMfaTrustedDevices());
-        
authProfileTO.getWebAuthnDeviceCredentials().addAll(authProfile.getWebAuthnDeviceCredentials());
-        return authProfileTO;
+        return new AuthProfileTO.Builder().
+                key(authProfile.getKey()).
+                owner(authProfile.getOwner()).
+                impersonationAccounts(authProfile.getImpersonationAccounts()).
+                googleMfaAuthTokens(authProfile.getGoogleMfaAuthTokens()).
+                googleMfaAuthAccounts(authProfile.getGoogleMfaAuthAccounts()).
+                mfaTrustedDevices(authProfile.getMfaTrustedDevices()).
+                
webAuthnDeviceCredentials(authProfile.getWebAuthnDeviceCredentials()).
+                consentDecisions(authProfile.getConsentDecisions()).
+                build();
     }
 
     @Override
@@ -63,6 +64,8 @@ public class AuthProfileDataBinderImpl implements 
AuthProfileDataBinder {
         authProfileTO.getMfaTrustedDevices().forEach(authProfile::add);
         authProfile.getWebAuthnDeviceCredentials().clear();
         authProfileTO.getWebAuthnDeviceCredentials().forEach(authProfile::add);
+        authProfile.getConsentDecisions().clear();
+        authProfileTO.getConsentDecisions().forEach(authProfile::add);
         return authProfile;
     }
 }
diff --git a/fit/wa-reference/src/main/resources/wa-embedded.properties 
b/fit/wa-reference/src/main/resources/wa-embedded.properties
index 8f9abcecf5..40d6658489 100644
--- a/fit/wa-reference/src/main/resources/wa-embedded.properties
+++ b/fit/wa-reference/src/main/resources/wa-embedded.properties
@@ -44,4 +44,4 @@ 
cas.tgc.crypto.encryption.key=mW6lMvsSo48eZ1Ntt74a-O9jjQQQ_OLUE24RVN2_A_sPX43mpB
 
cas.webflow.crypto.signing.key=Md6kkPlXx5L18TD0mFELpQXWnDbMffj-uPutPckMnAPPuJQEbfcLLYBnOynYIEDgnEpd7sxUwGYd8_sVYFMcjw
 cas.webflow.crypto.encryption.key=FhLgLpaPL8GVNuqqo7gtiw
 
-management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes
+management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent
diff --git a/src/main/asciidoc/reference-guide/concepts/authprofile.adoc 
b/src/main/asciidoc/reference-guide/concepts/authprofile.adoc
new file mode 100644
index 0000000000..d7e230e841
--- /dev/null
+++ b/src/main/asciidoc/reference-guide/concepts/authprofile.adoc
@@ -0,0 +1,28 @@
+//
+// 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.
+//
+=== Auth Profile
+
+When users log into the <<client-applications,web applications>> integrated 
with <<web-access,WA>>, the following
+information is tracked for <<admin-console,administration>> and 
<<enduser-application,self-management>>:
+
+* 
https://apereo.github.io/cas/7.3.x/authentication/Surrogate-Authentication.html[Surrogate
 Authentication^]
+* 
https://apereo.github.io/cas/7.3.x/mfa/GoogleAuthenticator-Authentication.html[Google
 Authenticator Authentication^]
+* 
https://apereo.github.io/cas/7.3.x/mfa/Multifactor-TrustedDevice-Authentication.html[Multifactor
 Authentication Trusted Devices^]
+* 
https://apereo.github.io/cas/7.3.x/mfa/FIDO2-WebAuthn-Authentication.html[FIDO2 
WebAuthn (Passkey) Multifactor Authentication^]
+* 
https://apereo.github.io/cas/7.3.x/integration/Attribute-Release-Consent.html[Attribute
 Consent^]
diff --git a/src/main/asciidoc/reference-guide/concepts/concepts.adoc 
b/src/main/asciidoc/reference-guide/concepts/concepts.adoc
index b2e15b3422..449335f66b 100644
--- a/src/main/asciidoc/reference-guide/concepts/concepts.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/concepts.adoc
@@ -54,6 +54,8 @@ include::passwordmanagement.adoc[]
 
 include::clientapplications.adoc[]
 
+include::authprofile.adoc[]
+
 include::domains.adoc[]
 
 include::implementations.adoc[]
diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml
index 70c6244bec..79c47989b4 100644
--- a/wa/starter/pom.xml
+++ b/wa/starter/pom.xml
@@ -254,6 +254,10 @@ under the License.
       <groupId>org.apereo.cas</groupId>
       <artifactId>cas-server-support-consent-webflow</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apereo.cas</groupId>
+      <artifactId>cas-server-support-consent-api</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.apereo.cas</groupId>
       <artifactId>cas-server-support-aup-webflow</artifactId>
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java
 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java
index 9f7232767b..809ba33071 100644
--- 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java
@@ -24,6 +24,7 @@ import org.apache.syncope.wa.bootstrap.WAProperties;
 import org.apache.syncope.wa.bootstrap.WARestClient;
 import org.apache.syncope.wa.starter.config.WARefreshContextJob;
 import org.apereo.cas.config.CasGoogleAuthenticatorLdapAutoConfiguration;
+import org.apereo.cas.config.CasJdbcPasswordManagementAutoConfiguration;
 import org.apereo.cas.configuration.CasConfigurationProperties;
 import org.apereo.cas.metadata.CasConfigurationPropertiesValidator;
 import 
org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerator;
@@ -52,7 +53,8 @@ import 
org.springframework.transaction.annotation.EnableTransactionManagement;
     DataSourceHealthContributorAutoConfiguration.class,
     DataJdbcRepositoriesAutoConfiguration.class,
     JmxAutoConfiguration.class,
-    CasGoogleAuthenticatorLdapAutoConfiguration.class
+    CasGoogleAuthenticatorLdapAutoConfiguration.class,
+    CasJdbcPasswordManagementAutoConfiguration.class
 })
 @EnableConfigurationProperties({ WAProperties.class, 
CasConfigurationProperties.class })
 @EnableAsync(proxyTargetClass = false)
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java
index 6aba695972..4452c96ea8 100644
--- 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java
@@ -42,6 +42,7 @@ import 
org.apache.syncope.wa.bootstrap.mapping.AttrReleaseMapper;
 import org.apache.syncope.wa.starter.actuate.SyncopeCoreHealthIndicator;
 import org.apache.syncope.wa.starter.actuate.SyncopeWAInfoContributor;
 import org.apache.syncope.wa.starter.audit.WAAuditTrailManager;
+import org.apache.syncope.wa.starter.consent.WAConsentRepository;
 import org.apache.syncope.wa.starter.events.WAEventRepository;
 import org.apache.syncope.wa.starter.gauth.WAGoogleMfaAuthCredentialRepository;
 import org.apache.syncope.wa.starter.gauth.WAGoogleMfaAuthTokenRepository;
@@ -77,6 +78,8 @@ import 
org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService;
 import org.apereo.cas.configuration.CasConfigurationProperties;
 import 
org.apereo.cas.configuration.model.support.mfa.gauth.LdapGoogleAuthenticatorMultifactorProperties;
 import 
org.apereo.cas.configuration.model.support.pm.PasswordManagementProperties;
+import org.apereo.cas.configuration.support.JpaBeans;
+import org.apereo.cas.consent.ConsentRepository;
 import org.apereo.cas.gauth.CasGoogleAuthenticator;
 import 
org.apereo.cas.gauth.credential.LdapGoogleAuthenticatorTokenCredentialRepository;
 import org.apereo.cas.oidc.jwks.generator.OidcJsonWebKeystoreGeneratorService;
@@ -106,6 +109,8 @@ import 
org.apereo.cas.util.spring.CasApplicationReadyListener;
 import org.apereo.cas.webauthn.storage.WebAuthnCredentialRepository;
 import org.ldaptive.ConnectionFactory;
 import org.pac4j.core.client.Client;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.annotation.Qualifier;
 import 
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -116,16 +121,20 @@ import 
org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.ScopedProxyMode;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
-import org.springframework.transaction.support.TransactionOperations;
+import org.springframework.transaction.support.TransactionTemplate;
 import org.springframework.web.client.RestTemplate;
 
 @Configuration(proxyBeanMethods = false)
 public class WAContext {
 
+    protected static final Logger LOG = 
LoggerFactory.getLogger(WAContext.class);
+
     public static final String CUSTOM_GOOGLE_AUTHENTICATOR_ACCOUNT_REGISTRY =
             "customGoogleAuthenticatorAccountRegistry";
 
@@ -422,24 +431,39 @@ public class WAContext {
     public PasswordManagementService jdbcPasswordChangeService(
             final CasConfigurationProperties casProperties,
             final ConfigurableApplicationContext ctx,
-            @Qualifier("jdbcPasswordManagementDataSource")
-            final DataSource jdbcPasswordManagementDataSource,
-            @Qualifier("jdbcPasswordManagementTransactionTemplate")
-            final TransactionOperations 
jdbcPasswordManagementTransactionTemplate,
             @Qualifier("passwordManagementCipherExecutor")
             final CipherExecutor<Serializable, String> 
passwordManagementCipherExecutor,
             @Qualifier(PasswordHistoryService.BEAN_NAME)
             final PasswordHistoryService passwordHistoryService) {
 
         PasswordManagementProperties pm = casProperties.getAuthn().getPm();
-        if (pm.getCore().isEnabled() && 
StringUtils.isNotBlank(pm.getJdbc().getUrl())) {
-            return new JdbcPasswordManagementService(
-                    passwordManagementCipherExecutor,
-                    casProperties,
-                    jdbcPasswordManagementDataSource,
-                    jdbcPasswordManagementTransactionTemplate,
-                    passwordHistoryService,
-                    
PasswordEncoderUtils.newPasswordEncoder(pm.getJdbc().getPasswordEncoder(), 
ctx));
+        if (pm.getCore().isEnabled()) {
+            try {
+                Class.forName(pm.getJdbc().getDriverClass());
+
+                DataSource jdbcPasswordManagementDataSource = 
JpaBeans.newDataSource(pm.getJdbc());
+                DataSourceTransactionManager 
jdbcPasswordManagementTransactionManager =
+                        new 
DataSourceTransactionManager(jdbcPasswordManagementDataSource);
+                TransactionTemplate jdbcPasswordManagementTransactionTemplate =
+                        new 
TransactionTemplate(jdbcPasswordManagementTransactionManager);
+                
jdbcPasswordManagementTransactionTemplate.setIsolationLevelName(
+                        pm.getJdbc().getIsolationLevelName());
+                
jdbcPasswordManagementTransactionTemplate.setPropagationBehaviorName(
+                        pm.getJdbc().getPropagationBehaviorName());
+
+                PasswordEncoder encoder = 
PasswordEncoderUtils.newPasswordEncoder(
+                        pm.getJdbc().getPasswordEncoder(), ctx);
+
+                return new JdbcPasswordManagementService(
+                        passwordManagementCipherExecutor,
+                        casProperties,
+                        jdbcPasswordManagementDataSource,
+                        jdbcPasswordManagementTransactionTemplate,
+                        passwordHistoryService,
+                        encoder);
+            } catch (ClassNotFoundException e) {
+                LOG.debug("{} is not available, disabling 
jdbcPasswordChangeService", pm.getJdbc().getDriverClass(), e);
+            }
         }
 
         return new 
NoOpPasswordManagementService(passwordManagementCipherExecutor, casProperties);
@@ -541,6 +565,12 @@ public class WAContext {
         return new WAWebAuthnCredentialRepository(casProperties, waRestClient);
     }
 
+    @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
+    @Bean
+    public ConsentRepository consentRepository(final WARestClient 
waRestClient) {
+        return new WAConsentRepository(waRestClient);
+    }
+
     @Bean
     public SurrogateAuthenticationService surrogateAuthenticationService(final 
WARestClient waRestClient) {
         return new WASurrogateAuthenticationService(waRestClient);
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/consent/WAConsentRepository.java
 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/consent/WAConsentRepository.java
new file mode 100644
index 0000000000..0e891e76b2
--- /dev/null
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/consent/WAConsentRepository.java
@@ -0,0 +1,124 @@
+/*
+ * 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.consent;
+
+import java.util.Collection;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
+import org.apache.syncope.common.rest.api.service.wa.ConsentDecisionService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.apereo.cas.authentication.Authentication;
+import org.apereo.cas.authentication.principal.Service;
+import org.apereo.cas.consent.ConsentDecision;
+import org.apereo.cas.consent.ConsentReminderOptions;
+import org.apereo.cas.consent.ConsentRepository;
+import org.apereo.cas.services.RegisteredService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WAConsentRepository implements ConsentRepository {
+
+    private static final long serialVersionUID = -3094119228321296264L;
+
+    protected static final Logger LOG = 
LoggerFactory.getLogger(WAConsentRepository.class);
+
+    protected static WAConsentDecision toWAConsentDecision(final 
ConsentDecision decision) {
+        return new WAConsentDecision.Builder(
+                decision.getId(), decision.getPrincipal(), 
decision.getService(), decision.getCreatedDate()).
+                
options(WAConsentDecision.ReminderOptions.valueOf(decision.getOptions().name())).
+                reminder(decision.getReminder()).
+                reminderTimeUnit(decision.getReminderTimeUnit()).
+                attributes(decision.getAttributes()).
+                build();
+    }
+
+    protected static ConsentDecision toConsentDecision(final String tenant, 
final WAConsentDecision waConsentDecision) {
+        ConsentDecision consentDecision = new ConsentDecision();
+        consentDecision.setId(waConsentDecision.getId());
+        consentDecision.setPrincipal(waConsentDecision.getPrincipal());
+        consentDecision.setService(waConsentDecision.getService());
+        consentDecision.setCreatedDate(waConsentDecision.getCreatedDate());
+        
consentDecision.setOptions(ConsentReminderOptions.valueOf(waConsentDecision.getOptions().getValue()));
+        consentDecision.setReminder(waConsentDecision.getReminder());
+        
consentDecision.setReminderTimeUnit(waConsentDecision.getReminderTimeUnit());
+        consentDecision.setTenant(tenant);
+        consentDecision.setAttributes(waConsentDecision.getAttributes());
+        return consentDecision;
+    }
+
+    protected final WARestClient waRestClient;
+
+    public WAConsentRepository(final WARestClient waRestClient) {
+        this.waRestClient = waRestClient;
+    }
+
+    @Override
+    public ConsentDecision findConsentDecision(
+            final Service service,
+            final RegisteredService registeredService,
+            final Authentication authentication) {
+
+        try {
+            WAConsentDecision waContentDecision = 
waRestClient.getService(ConsentDecisionService.class).
+                    read(authentication.getPrincipal().getId(), 
service.getId());
+            return 
toConsentDecision(waRestClient.getSyncopeClient().getDomain(), 
waContentDecision);
+        } catch (Exception e) {
+            LOG.error("While attempting to find ConsentDecision for principal 
{} and service {}",
+                    authentication.getPrincipal().getId(), service.getId(), e);
+            return null;
+        }
+    }
+
+    @Override
+    public Collection<? extends ConsentDecision> findConsentDecisions(final 
String principal) {
+        return 
waRestClient.getService(ConsentDecisionService.class).read(principal).getResult().stream().
+                map(wcd -> 
toConsentDecision(waRestClient.getSyncopeClient().getDomain(), wcd)).
+                toList();
+    }
+
+    @Override
+    public Collection<? extends ConsentDecision> findConsentDecisions() {
+        return 
waRestClient.getService(ConsentDecisionService.class).list().getResult().stream().
+                map(wcd -> 
toConsentDecision(waRestClient.getSyncopeClient().getDomain(), wcd)).
+                toList();
+    }
+
+    @Override
+    public ConsentDecision storeConsentDecision(final ConsentDecision 
decision) throws Throwable {
+        waRestClient.getService(ConsentDecisionService.class).
+                store(decision.getPrincipal(), toWAConsentDecision(decision));
+        return decision;
+    }
+
+    @Override
+    public boolean deleteConsentDecision(final long id, final String 
principal) throws Throwable {
+        
waRestClient.getService(ConsentDecisionService.class).delete(principal, id);
+        return true;
+    }
+
+    @Override
+    public boolean deleteConsentDecisions(final String principal) throws 
Throwable {
+        
waRestClient.getService(ConsentDecisionService.class).delete(principal);
+        return true;
+    }
+
+    @Override
+    public void deleteAll() throws Throwable {
+        waRestClient.getService(ConsentDecisionService.class).deleteAll();
+    }
+}
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthCredentialRepository.java
 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthCredentialRepository.java
index f94f7d1da2..563a76f8b2 100644
--- 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthCredentialRepository.java
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthCredentialRepository.java
@@ -39,16 +39,7 @@ public class WAGoogleMfaAuthCredentialRepository extends 
BaseGoogleAuthenticator
 
     protected static final Logger LOG = 
LoggerFactory.getLogger(WAGoogleMfaAuthTokenRepository.class);
 
-    protected final WARestClient waRestClient;
-
-    public WAGoogleMfaAuthCredentialRepository(
-            final WARestClient waRestClient, final CasGoogleAuthenticator 
googleAuthenticator) {
-
-        super(CipherExecutor.noOpOfStringToString(), 
CipherExecutor.noOpOfNumberToNumber(), googleAuthenticator);
-        this.waRestClient = waRestClient;
-    }
-
-    protected GoogleAuthenticatorAccount mapGoogleMfaAuthAccount(final 
GoogleMfaAuthAccount gmfaa) {
+    protected static GoogleAuthenticatorAccount mapGoogleMfaAuthAccount(final 
GoogleMfaAuthAccount gmfaa) {
         return GoogleAuthenticatorAccount.builder().
                 id(gmfaa.getId()).
                 name(gmfaa.getName()).
@@ -61,6 +52,28 @@ public class WAGoogleMfaAuthCredentialRepository extends 
BaseGoogleAuthenticator
                 build();
     }
 
+    protected static GoogleMfaAuthAccount mapOneTimeTokenAccount(final 
OneTimeTokenAccount otta) {
+        return new GoogleMfaAuthAccount.Builder().
+                id(otta.getId()).
+                name(otta.getName()).
+                username(otta.getUsername()).
+                secretKey(otta.getSecretKey()).
+                validationCode(otta.getValidationCode()).
+                
scratchCodes(otta.getScratchCodes().stream().map(Number::intValue).toList()).
+                registrationDate(ZonedDateTime.now()).
+                source(otta.getSource()).
+                build();
+    }
+
+    protected final WARestClient waRestClient;
+
+    public WAGoogleMfaAuthCredentialRepository(
+            final WARestClient waRestClient, final CasGoogleAuthenticator 
googleAuthenticator) {
+
+        super(CipherExecutor.noOpOfStringToString(), 
CipherExecutor.noOpOfNumberToNumber(), googleAuthenticator);
+        this.waRestClient = waRestClient;
+    }
+
     @Override
     public OneTimeTokenAccount get(final long id) {
         try {
@@ -81,7 +94,7 @@ public class WAGoogleMfaAuthCredentialRepository extends 
BaseGoogleAuthenticator
             return 
waRestClient.getService(GoogleMfaAuthAccountService.class).read(username).
                     getResult().stream().
                     filter(account -> account.getId() == id).
-                    map(this::mapGoogleMfaAuthAccount).
+                    
map(WAGoogleMfaAuthCredentialRepository::mapGoogleMfaAuthAccount).
                     findFirst().
                     orElse(null);
         } catch (SyncopeClientException e) {
@@ -99,7 +112,7 @@ public class WAGoogleMfaAuthCredentialRepository extends 
BaseGoogleAuthenticator
         try {
             return 
waRestClient.getService(GoogleMfaAuthAccountService.class).read(username).
                     getResult().stream().
-                    map(this::mapGoogleMfaAuthAccount).
+                    
map(WAGoogleMfaAuthCredentialRepository::mapGoogleMfaAuthAccount).
                     toList();
         } catch (SyncopeClientException e) {
             if (e.getType() == ClientExceptionType.NotFound) {
@@ -115,34 +128,19 @@ public class WAGoogleMfaAuthCredentialRepository extends 
BaseGoogleAuthenticator
     public Collection<? extends OneTimeTokenAccount> load() {
         return 
waRestClient.getService(GoogleMfaAuthAccountService.class).list().
                 getResult().stream().
-                map(this::mapGoogleMfaAuthAccount).
+                
map(WAGoogleMfaAuthCredentialRepository::mapGoogleMfaAuthAccount).
                 toList();
     }
 
-    protected GoogleMfaAuthAccount mapOneTimeTokenAccount(final 
OneTimeTokenAccount otta) {
-        return new GoogleMfaAuthAccount.Builder().
-                id(otta.getId()).
-                name(otta.getName()).
-                username(otta.getUsername()).
-                secretKey(otta.getSecretKey()).
-                validationCode(otta.getValidationCode()).
-                
scratchCodes(otta.getScratchCodes().stream().map(Number::intValue).toList()).
-                registrationDate(ZonedDateTime.now()).
-                source(otta.getSource()).
-                build();
-    }
-
     @Override
-    public OneTimeTokenAccount save(final OneTimeTokenAccount otta) {
-        GoogleMfaAuthAccount account = mapOneTimeTokenAccount(otta);
-        
waRestClient.getService(GoogleMfaAuthAccountService.class).create(account);
-        return otta;
+    public OneTimeTokenAccount save(final OneTimeTokenAccount tokenAccount) {
+        
waRestClient.getService(GoogleMfaAuthAccountService.class).create(mapOneTimeTokenAccount(tokenAccount));
+        return tokenAccount;
     }
 
     @Override
     public OneTimeTokenAccount update(final OneTimeTokenAccount tokenAccount) {
-        GoogleMfaAuthAccount acct = mapOneTimeTokenAccount(tokenAccount);
-        
waRestClient.getService(GoogleMfaAuthAccountService.class).update(acct);
+        
waRestClient.getService(GoogleMfaAuthAccountService.class).update(mapOneTimeTokenAccount(tokenAccount));
         return tokenAccount;
     }
 
diff --git a/wa/starter/src/main/resources/wa.properties 
b/wa/starter/src/main/resources/wa.properties
index 8ab439b6cd..6adbeefd1e 100644
--- a/wa/starter/src/main/resources/wa.properties
+++ b/wa/starter/src/main/resources/wa.properties
@@ -37,7 +37,7 @@ 
spring.web.resources.static-locations=classpath:/thymeleaf/static,classpath:/syn
 
 cas.monitor.endpoints.endpoint.defaults.access=AUTHENTICATED
 management.endpoints.access.default=UNRESTRICTED
-management.endpoints.web.exposure.include=info,health,env,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes
+management.endpoints.web.exposure.include=info,health,env,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent
 management.endpoint.health.show-details=ALWAYS
 management.endpoint.env.show-values=WHEN_AUTHORIZED
 spring.cloud.discovery.client.health-indicator.enabled=false
diff --git a/wa/starter/src/test/resources/debug/wa-debug.properties 
b/wa/starter/src/test/resources/debug/wa-debug.properties
index 22a486ef8f..35bd946735 100644
--- a/wa/starter/src/test/resources/debug/wa-debug.properties
+++ b/wa/starter/src/test/resources/debug/wa-debug.properties
@@ -21,7 +21,7 @@ 
keymaster.address=https://localhost:9443/syncope/rest/keymaster
 keymaster.username=${anonymousUser}
 keymaster.password=${anonymousKey}
 
-management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes
+management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent
 
 cas.server.name=http://localhost:8080
 cas.server.prefix=${cas.server.name}/syncope-wa

Reply via email to