keycloak-aplcache

Changes

forms/common-themes/src/main/resources/theme/base/account/draft.access.ftl 82(+0 -82)

Details

diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml
index 2a066d0..fbea205 100755
--- a/connections/jpa/src/main/resources/META-INF/persistence.xml
+++ b/connections/jpa/src/main/resources/META-INF/persistence.xml
@@ -20,6 +20,9 @@
         <class>org.keycloak.models.jpa.entities.IdentityProviderMapperEntity</class>
         <class>org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.ProtocolMapperEntity</class>
+        <class>org.keycloak.models.jpa.entities.GrantedConsentEntity</class>
+        <class>org.keycloak.models.jpa.entities.GrantedConsentRoleEntity</class>
+        <class>org.keycloak.models.jpa.entities.GrantedConsentProtocolMapperEntity</class>
 
         <!-- JpaUserSessionProvider -->
         <class>org.keycloak.models.sessions.jpa.entities.ClientSessionEntity</class>
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml
index aa78acb..a1698dc 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml
@@ -35,10 +35,43 @@
         <addColumn tableName="CREDENTIAL">
             <column name="CREATED_DATE" type="BIGINT"/>
         </addColumn>
+        <createTable tableName="GRANTED_CONSENT">
+            <column name="ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="CLIENT_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="USER_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+        </createTable>
+        <createTable tableName="GRANTED_CONSENT_ROLE">
+            <column name="GRANTED_CONSENT_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="ROLE_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+        </createTable>
+        <createTable tableName="GRANTED_CONSENT_PROT_MAPPER">
+            <column name="GRANTED_CONSENT_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="PROTOCOL_MAPPER_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+        </createTable>
         <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_IDPM" tableName="IDENTITY_PROVIDER_MAPPER"/>
         <addPrimaryKey columnNames="IDP_MAPPER_ID, NAME" constraintName="CONSTRAINT_IDPMConfig" tableName="IDP_MAPPER_CONFIG"/>
+        <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_GRNTCSNT_PM" tableName="GRANTED_CONSENT"/>
+        <addPrimaryKey columnNames="GRANTED_CONSENT_ID, ROLE_ID" constraintName="CONSTRAINT_GRNTCSNT_ROLE_PM" tableName="GRANTED_CONSENT_ROLE"/>
+        <addPrimaryKey columnNames="GRANTED_CONSENT_ID, PROTOCOL_MAPPER_ID" constraintName="CONSTRAINT_GRNTCSNT_PRM_PM" tableName="GRANTED_CONSENT_PROT_MAPPER"/>
         <addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="IDENTITY_PROVIDER_MAPPER" constraintName="FK_IDPM_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
         <addForeignKeyConstraint baseColumnNames="IDP_MAPPER_ID" baseTableName="IDP_MAPPER_CONFIG" constraintName="FK_IDPMConfig" referencedColumnNames="ID" referencedTableName="IDENTITY_PROVIDER_MAPPER"/>
+        <addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="GRANTED_CONSENT" constraintName="FK_GRNTCSNT_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
+        <addForeignKeyConstraint baseColumnNames="GRANTED_CONSENT_ID" baseTableName="GRANTED_CONSENT_ROLE" constraintName="FK_GRNTCSNT_ROLE_GR" referencedColumnNames="ID" referencedTableName="GRANTED_CONSENT"/>
+        <addForeignKeyConstraint baseColumnNames="GRANTED_CONSENT_ID" baseTableName="GRANTED_CONSENT_PROT_MAPPER" constraintName="FK_GRNTCSNT_PRM_GR" referencedColumnNames="ID" referencedTableName="GRANTED_CONSENT"/>
 
         <addColumn tableName="CLIENT">
             <column name="CONSENT_REQUIRED" type="BOOLEAN" defaultValueBoolean="false">
@@ -68,6 +101,7 @@
 
         <dropUniqueConstraint tableName="KEYCLOAK_ROLE" constraintName="UK_J3RWUVD56ONTGSUHOGM184WW2"/>
         <addUniqueConstraint columnNames="NAME,CLIENT_REALM_CONSTRAINT" constraintName="UK_J3RWUVD56ONTGSUHOGM184WW2-2" tableName="KEYCLOAK_ROLE"/>
+        <addUniqueConstraint columnNames="CLIENT_ID, USER_ID" constraintName="UK_JKUWUVD56ONTGSUHOGM8UEWRT" tableName="GRANTED_CONSENT"/>
 
     </changeSet>
 </databaseChangeLog>
diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java
index 17a24cf..cee7475 100755
--- a/events/api/src/main/java/org/keycloak/events/Details.java
+++ b/events/api/src/main/java/org/keycloak/events/Details.java
@@ -23,5 +23,6 @@ public interface Details {
     String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id";
     String NODE_HOST = "node_host";
     String REASON = "reason";
+    String REVOKED_CLIENT = "revoked_client";
 
 }
diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java
index 680a06e..dc40379 100755
--- a/events/api/src/main/java/org/keycloak/events/EventType.java
+++ b/events/api/src/main/java/org/keycloak/events/EventType.java
@@ -39,6 +39,8 @@ public enum EventType {
     REMOVE_TOTP(true),
     REMOVE_TOTP_ERROR(true),
 
+    REVOKE_GRANT(true),
+
     SEND_VERIFY_EMAIL(true),
     SEND_VERIFY_EMAIL_ERROR(true),
     SEND_RESET_PASSWORD(true),
diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
index d316574..3e236dd 100644
--- a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
+++ b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
@@ -5,6 +5,6 @@ package org.keycloak.account;
  */
 public enum AccountPages {
 
-    ACCOUNT, PASSWORD, TOTP, FEDERATED_IDENTITY, LOG, SESSIONS;
+    ACCOUNT, PASSWORD, TOTP, FEDERATED_IDENTITY, LOG, SESSIONS, ACCESS;
 
 }
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
index fe38f2c..ba95348 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
@@ -20,6 +20,7 @@ import javax.ws.rs.core.UriInfo;
 import org.jboss.logging.Logger;
 import org.keycloak.account.AccountPages;
 import org.keycloak.account.AccountProvider;
+import org.keycloak.account.freemarker.model.AccessBean;
 import org.keycloak.account.freemarker.model.AccountBean;
 import org.keycloak.account.freemarker.model.AccountFederatedIdentityBean;
 import org.keycloak.account.freemarker.model.FeaturesBean;
@@ -37,6 +38,7 @@ import org.keycloak.freemarker.FreeMarkerUtil;
 import org.keycloak.freemarker.LocaleHelper;
 import org.keycloak.freemarker.Theme;
 import org.keycloak.freemarker.ThemeProvider;
+import org.keycloak.freemarker.beans.AdvancedMessageFormatterMethod;
 import org.keycloak.freemarker.beans.LocaleBean;
 import org.keycloak.freemarker.beans.MessageBean;
 import org.keycloak.freemarker.beans.MessageFormatterMethod;
@@ -183,6 +185,10 @@ public class FreeMarkerAccountProvider implements AccountProvider {
             case SESSIONS:
                 attributes.put("sessions", new SessionsBean(realm, sessions));
                 break;
+            case ACCESS:
+                attributes.put("access", new AccessBean(realm, user, uriInfo.getBaseUri(), stateChecker));
+                attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
+                break;
             case PASSWORD:
                 attributes.put("password", new PasswordBean(passwordSet));
         }
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccessBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccessBean.java
new file mode 100644
index 0000000..58cea2f
--- /dev/null
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccessBean.java
@@ -0,0 +1,85 @@
+package org.keycloak.account.freemarker.model;
+
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.util.MultivaluedHashMap;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AccessBean {
+
+    private List<ClientGrantBean> clientGrants = new LinkedList<ClientGrantBean>();
+
+    public AccessBean(RealmModel realm, UserModel user, URI baseUri, String stateChecker) {
+        List<GrantedConsentModel> grantedConsents = user.getGrantedConsents();
+        for (GrantedConsentModel consent : grantedConsents) {
+            ClientModel client = realm.getClientById(consent.getClientId());
+
+            List<RoleModel> realmRolesGranted = new LinkedList<RoleModel>();
+            MultivaluedHashMap<String, RoleModel> resourceRolesGranted = new MultivaluedHashMap<String, RoleModel>();
+            for (String roleId : consent.getGrantedRoles()) {
+                RoleModel role = realm.getRoleById(roleId);
+                if (role.getContainer() instanceof RealmModel) {
+                    realmRolesGranted.add(role);
+                } else {
+                    resourceRolesGranted.add(((ClientModel) role.getContainer()).getClientId(), role);
+                }
+            }
+
+            List<String> claimsGranted = new LinkedList<String>();
+            for (String protocolMapperId : consent.getGrantedProtocolMappers()) {
+                ProtocolMapperModel protocolMapper = client.getProtocolMapperById(protocolMapperId);
+                claimsGranted.add(protocolMapper.getConsentText());
+            }
+
+            ClientGrantBean clientGrant = new ClientGrantBean(realmRolesGranted, resourceRolesGranted, client, claimsGranted);
+            clientGrants.add(clientGrant);
+        }
+    }
+
+    public List<ClientGrantBean> getClientGrants() {
+        return clientGrants;
+    }
+
+    public static class ClientGrantBean {
+
+        private final List<RoleModel> realmRolesGranted;
+        private final MultivaluedHashMap<String, RoleModel> resourceRolesGranted;
+        private final ClientModel client;
+        private final List<String> claimsGranted;
+
+        public ClientGrantBean(List<RoleModel> realmRolesGranted, MultivaluedHashMap<String, RoleModel> resourceRolesGranted,
+                               ClientModel client, List<String> claimsGranted) {
+            this.realmRolesGranted = realmRolesGranted;
+            this.resourceRolesGranted = resourceRolesGranted;
+            this.client = client;
+            this.claimsGranted = claimsGranted;
+        }
+
+        public List<RoleModel> getRealmRolesGranted() {
+            return realmRolesGranted;
+        }
+
+        public MultivaluedHashMap<String, RoleModel> getResourceRolesGranted() {
+            return resourceRolesGranted;
+        }
+
+        public ClientModel getClient() {
+            return client;
+        }
+
+        public List<String> getClaimsGranted() {
+            return claimsGranted;
+        }
+
+    }
+}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
index 001ffde..40ea59f 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
@@ -59,6 +59,10 @@ public class UrlBean {
         return Urls.accountSessionsLogoutPage(baseQueryURI, realm, stateChecker).toString();
     }
 
+    public String getRevokeClientUrl() {
+        return Urls.accountRevokeClientPage(baseQueryURI, realm).toString();
+    }
+
     public String getTotpRemoveUrl() {
         return Urls.accountTotpRemove(baseQueryURI, realm, stateChecker).toString();
     }
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
index 1d33f8a..847a3e6 100644
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
@@ -21,6 +21,8 @@ public class Templates {
                 return "log.ftl";
             case SESSIONS:
                 return "sessions.ftl";
+            case ACCESS:
+                return "access.ftl";
             default:
                 throw new IllegalArgumentException();
         }
diff --git a/forms/common-themes/src/main/resources/theme/base/account/access.ftl b/forms/common-themes/src/main/resources/theme/base/account/access.ftl
new file mode 100755
index 0000000..dc3cc41
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/account/access.ftl
@@ -0,0 +1,55 @@
+<#import "template.ftl" as layout>
+<@layout.mainLayout active='access' bodyClass='access'; section>
+
+    <div class="row">
+        <div class="col-md-10">
+            <h2>${msg("accessHtmlTitle")}</h2>
+        </div>
+    </div>
+
+    <form action="${url.revokeClientUrl}" method="post">
+        <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
+
+        <table class="table table-striped table-bordered">
+            <thead>
+              <tr>
+                <td>${msg("client")}</td>
+                <td>${msg("grantedPersonalInfo")}</td>
+                <td>${msg("grantedPermissions")}</td>
+                <td>${msg("action")}</td>
+              </tr>
+            </thead>
+
+            <tbody>
+              <#list access.clientGrants as clientGrant>
+                <tr>
+                    <td><#if clientGrant.client.baseUrl??><a href="${clientGrant.client.baseUrl}">${clientGrant.client.clientId}</a><#else>${clientGrant.client.clientId}</#if></td>
+                    <td>
+                        <#list clientGrant.claimsGranted as claim>
+                            ${advancedMsg(claim)}<#if claim_has_next>, </#if>
+                        </#list>
+                    </td>
+                    <td>
+                        <#list clientGrant.realmRolesGranted as role>
+                            <#if role.description??>${advancedMsg(role.description)}<#else>${advancedMsg(role.name)}</#if>
+                            <#if role_has_next>, </#if>
+                        </#list>
+                        <#list clientGrant.resourceRolesGranted?keys as resource>
+                            <#if clientGrant.realmRolesGranted?has_content>, </#if>
+                            <#list clientGrant.resourceRolesGranted[resource] as role>
+                                <#if role.description??>${advancedMsg(role.description)}<#else>${advancedMsg(role.name)}</#if>
+                                ${msg("inResource", resource)}
+                                <#if role_has_next>, </#if>
+                            </#list>
+                        </#list>
+                    </td>
+                    <td>
+                        <button type='submit' class='btn btn-primary' id='revoke-${clientGrant.client.clientId}' name='clientId' value="${clientGrant.client.id}">${msg("revoke")}</button>
+                    </td>
+                </tr>
+              </#list>
+            </tbody>
+        </table>
+    </form>
+
+</@layout.mainLayout>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
index 2c72297..9749f25 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -12,20 +12,45 @@ changePasswordHtmlTitle=Change Password
 sessionsHtmlTitle=Sessions
 accountManagementTitle=Keycloak Account Management
 authenticatorTitle=Authenticator
+accessHtmlTitle=Manage Granted Permissions
 
 authenticatorCode=One-time code
 email=Email
 firstName=First name
+givenName=Given name
+fullName=Full name
 lastName=Last name
+familyName=Family name
 password=Password
 passwordConfirm=Confirmation
 passwordNew=New Password
 username=Username
+address=Address
 street=Street
 locality=City or Locality
 region=State, Province, or Region
 postal_code=Zip or Postal code
 country=Country
+emailVerified=Email verified
+gssDelegationCredential=gss delegation credential
+
+role_admin=Admin
+role_realm-admin=Realm Admin
+role_create-realm=Create realm
+role_view-realm=View realm
+role_view-users=View users
+role_view-applications=View applications
+role_view-clients=View clients
+role_view-events=View events
+role_view-identity-providers=View identity providers
+role_manage-realm=Manage realm
+role_manage-users=Manage users
+role_manage-applications=Manage applications
+role_manage-identity-providers=Manage identity providers
+role_manage-clients=Manage clients
+role_manage-events=Manage events
+role_view-profile=View profile
+
 
 requiredFields=Required fields
 allFieldsRequired=All fields required
@@ -49,6 +74,13 @@ federatedIdentity=Federated Identity
 authenticator=Authenticator
 sessions=Sessions
 log=Log
+access=Access
+
+grantedPersonalInfo=Granted Personal Info
+grantedPermissions=Granted Permissions
+action=Action
+inResource=in <strong>{0}</strong>
+revoke=Revoke Access
 
 configureAuthenticators=Configured Authenticators
 mobile=Mobile
@@ -74,6 +106,8 @@ readOnlyPasswordMessage=You can''t update your password as your account is read 
 successTotpMessage=Mobile authenticator configured.
 successTotpRemovedMessage=Mobile authenticator removed.
 
+successGrantRevokedMessage=Access revoked successfully.
+
 accountUpdatedMessage=Your account has been updated.
 accountPasswordUpdatedMessage=Your password has been updated.
 
diff --git a/forms/common-themes/src/main/resources/theme/base/account/template.ftl b/forms/common-themes/src/main/resources/theme/base/account/template.ftl
index d461118..1319b6f 100644
--- a/forms/common-themes/src/main/resources/theme/base/account/template.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/account/template.ftl
@@ -56,6 +56,7 @@
                 <li class="<#if active=='totp'>active</#if>"><a href="${url.totpUrl}">${msg("authenticator")}</a></li>
                 <#if features.identityFederation><li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">${msg("federatedIdentity")}</a></li></#if>
                 <li class="<#if active=='sessions'>active</#if>"><a href="${url.sessionsUrl}">${msg("sessions")}</a></li>
+                <li class="<#if active=='access'>active</#if>"><a href="${url.accessUrl}">${msg("access")}</a></li>
                 <#if features.log><li class="<#if active=='log'>active</#if>"><a href="${url.logUrl}">${msg("log")}</a></li></#if>
             </ul>
         </div>
diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl
index 9b0637b..6136e8b 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl
@@ -1,4 +1,3 @@
-<#-- TODO: Only a placeholder, implementation needed -->
 <#import "template.ftl" as layout>
 <@layout.registrationLayout bodyClass="oauth"; section>
     <#if section = "title">
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
index 8e5ef24..adfd3f8 100755
--- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
+++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
@@ -10,6 +10,7 @@ import javax.ws.rs.core.UriInfo;
 
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
@@ -41,7 +42,7 @@ public interface LoginFormsProvider extends Provider {
 
     public LoginFormsProvider setClientSessionCode(String accessCode);
 
-    public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested);
+    public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappers);
     public LoginFormsProvider setAccessRequest(String message);
 
     /**
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
index 7e5cb78..3c0f56f 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -32,6 +32,7 @@ import org.keycloak.login.freemarker.model.UrlBean;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
@@ -66,6 +67,7 @@ import java.util.concurrent.TimeUnit;
     private Response.Status status;
     private List<RoleModel> realmRolesRequested;
     private MultivaluedMap<String, RoleModel> resourceRolesRequested;
+    private List<ProtocolMapperModel> protocolMappersRequested;
     private MultivaluedMap<String, String> queryParams;
     private Map<String, String> httpResponseHeaders = new HashMap<String, String>();
     private String accessRequestMessage;
@@ -243,7 +245,7 @@ import java.util.concurrent.TimeUnit;
                 attributes.put("register", new RegisterBean(formData));
                 break;
             case OAUTH_GRANT:
-                attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, this.accessRequestMessage));
+                attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, protocolMappersRequested, this.accessRequestMessage));
                 attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
                 break;
             case CODE:
@@ -366,9 +368,10 @@ import java.util.concurrent.TimeUnit;
     }
 
     @Override
-    public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested) {
+    public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappersRequested) {
         this.realmRolesRequested = realmRolesRequested;
         this.resourceRolesRequested = resourceRolesRequested;
+        this.protocolMappersRequested = protocolMappersRequested;
         return this;
     }
 
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java
index 431a606..98cd164 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java
@@ -42,20 +42,18 @@ public class OAuthGrantBean {
     private ClientModel client;
     private List<String> claimsRequested;
 
-    public OAuthGrantBean(String code, ClientSessionModel clientSession, ClientModel client, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested, String accessRequestMessage) {
+    public OAuthGrantBean(String code, ClientSessionModel clientSession, ClientModel client, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested,
+                          List<ProtocolMapperModel> protocolMappersRequested, String accessRequestMessage) {
         this.code = code;
         this.client = client;
         this.realmRolesRequested = realmRolesRequested;
         this.resourceRolesRequested = resourceRolesRequested;
         this.accessRequestMessage = accessRequestMessage;
 
-        // todo support locale
         List<String> claims = new LinkedList<String>();
-        if (clientSession != null) {
-            for (ProtocolMapperModel model : client.getProtocolMappers()) {
-                if (model.isConsentRequired() && model.getProtocol().equals(clientSession.getAuthMethod()) && model.getConsentText() != null) {
-                    claims.add(model.getConsentText());
-                }
+        if (protocolMappersRequested != null) {
+            for (ProtocolMapperModel model : protocolMappersRequested) {
+                claims.add(model.getConsentText());
             }
         }
         if (claims.size() > 0) this.claimsRequested = claims;
diff --git a/model/api/src/main/java/org/keycloak/models/GrantedConsentModel.java b/model/api/src/main/java/org/keycloak/models/GrantedConsentModel.java
new file mode 100644
index 0000000..e036655
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/GrantedConsentModel.java
@@ -0,0 +1,47 @@
+package org.keycloak.models;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GrantedConsentModel {
+
+    private final String clientId;
+    private Set<String> protocolMapperIds = new HashSet<String>();
+    private Set<String> roleIds = new HashSet<String>();
+
+    public GrantedConsentModel(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void addGrantedRole(String roleId) {
+        roleIds.add(roleId);
+    }
+
+    public Set<String> getGrantedRoles() {
+        return roleIds;
+    }
+
+    public boolean isRoleGranted(String roleId) {
+        return roleIds.contains(roleId);
+    }
+
+    public void addGrantedProtocolMapper(String protocolMapperId) {
+        protocolMapperIds.add(protocolMapperId);
+    }
+
+    public Set<String> getGrantedProtocolMappers() {
+        return protocolMapperIds;
+    }
+
+    public boolean isProtocolMapperGranted(String protocolMapperId) {
+        return protocolMapperIds.contains(protocolMapperId);
+    }
+
+}
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
index 94d0226..b111433 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -320,6 +320,16 @@ public class UserFederationManager implements UserProvider {
         session.userStorage().preRemove(realm, role);
     }
 
+    @Override
+    public void preRemove(RealmModel realm, ClientModel client) {
+        session.userStorage().preRemove(realm, client);
+    }
+
+    @Override
+    public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+        session.userStorage().preRemove(client, protocolMapper);
+    }
+
     public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) {
         if (credential.getType().equals(UserCredentialModel.PASSWORD)) {
             if (realm.getPasswordPolicy() != null) {
diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java
index 770cf03..6ad716c 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -75,9 +75,11 @@ public interface UserModel {
     String getFederationLink();
     void setFederationLink(String link);
 
-
-
-
+    GrantedConsentModel addGrantedConsent(GrantedConsentModel consent);
+    GrantedConsentModel getGrantedConsentByClient(String clientId);
+    List<GrantedConsentModel> getGrantedConsents();
+    void updateGrantedConsent(GrantedConsentModel consent);
+    boolean revokeGrantedConsentForClient(String clientId);
 
     public static enum RequiredAction {
         VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD
diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java
index b508638..5e78cd1 100755
--- a/model/api/src/main/java/org/keycloak/models/UserProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java
@@ -41,6 +41,9 @@ public interface UserProvider extends Provider {
 
     void preRemove(RealmModel realm, RoleModel role);
 
+    void preRemove(RealmModel realm, ClientModel client);
+    void preRemove(ClientModel realm, ProtocolMapperModel protocolMapper);
+
     boolean validCredentials(RealmModel realm, UserModel user, List<UserCredentialModel> input);
     boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input);
     CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
index 0e84ca9..83b6add 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
@@ -1,6 +1,7 @@
 package org.keycloak.models.utils;
 
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserCredentialValueModel;
@@ -185,4 +186,29 @@ public class UserModelDelegate implements UserModel {
     public void setFederationLink(String link) {
         delegate.setFederationLink(link);
     }
+
+    @Override
+    public GrantedConsentModel addGrantedConsent(GrantedConsentModel consent) {
+        return delegate.addGrantedConsent(consent);
+    }
+
+    @Override
+    public GrantedConsentModel getGrantedConsentByClient(String clientId) {
+        return delegate.getGrantedConsentByClient(clientId);
+    }
+
+    @Override
+    public List<GrantedConsentModel> getGrantedConsents() {
+        return delegate.getGrantedConsents();
+    }
+
+    @Override
+    public void updateGrantedConsent(GrantedConsentModel consent) {
+        delegate.updateGrantedConsent(consent);
+    }
+
+    @Override
+    public boolean revokeGrantedConsentForClient(String clientId) {
+        return delegate.revokeGrantedConsentForClient(clientId);
+    }
 }
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
index 1ae5f12..3033eea 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
@@ -16,10 +16,11 @@
  */
 package org.keycloak.models.file.adapter;
 
-import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientModel;
 
 import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
 
+import org.keycloak.models.GrantedConsentModel;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
@@ -431,6 +432,35 @@ public class UserAdapter implements UserModel, Comparable {
     }
 
     @Override
+    public GrantedConsentModel addGrantedConsent(GrantedConsentModel consent) {
+        // TODO
+        return null;
+    }
+
+    @Override
+    public GrantedConsentModel getGrantedConsentByClient(String clientId) {
+        // TODO
+        return null;
+    }
+
+    @Override
+    public List<GrantedConsentModel> getGrantedConsents() {
+        // TODO
+        return null;
+    }
+
+    @Override
+    public void updateGrantedConsent(GrantedConsentModel consent) {
+        // TODO
+    }
+
+    @Override
+    public boolean revokeGrantedConsentForClient(String clientId) {
+        // TODO
+        return false;
+    }
+
+    @Override
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || !(o instanceof UserModel)) return false;
diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
index 850d1a7..5c50a59 100644
--- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
+++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
@@ -19,6 +19,8 @@ package org.keycloak.models.file;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
+
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.file.adapter.UserAdapter;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.KeycloakSession;
@@ -385,6 +387,16 @@ public class FileUserProvider implements UserProvider {
     }
 
     @Override
+    public void preRemove(RealmModel realm, ClientModel client) {
+        // TODO
+    }
+
+    @Override
+    public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+        // TODO
+    }
+
+    @Override
     public boolean validCredentials(RealmModel realm, UserModel user, List<UserCredentialModel> input) {
         return CredentialValidation.validCredentials(realm, user, input);
     }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
index 97f3c6c..16c77d1 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
@@ -1,8 +1,10 @@
 package org.keycloak.models.cache;
 
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.CredentialValidationOutput;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.FederatedIdentityModel;
@@ -310,4 +312,14 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
         realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
         getDelegate().preRemove(realm, link);
     }
+
+    @Override
+    public void preRemove(RealmModel realm, ClientModel client) {
+        getDelegate().preRemove(realm, client);
+    }
+
+    @Override
+    public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+        getDelegate().preRemove(client, protocolMapper);
+    }
 }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
index 857b3e8..b0b7069 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
@@ -1,7 +1,9 @@
 package org.keycloak.models.cache;
 
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.CredentialValidationOutput;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.FederatedIdentityModel;
@@ -175,4 +177,14 @@ public class NoCacheUserProvider implements CacheUserProvider {
     public void preRemove(RealmModel realm, RoleModel role) {
         getDelegate().preRemove(realm, role);
     }
+
+    @Override
+    public void preRemove(RealmModel realm, ClientModel client) {
+        getDelegate().preRemove(realm, client);
+    }
+
+    @Override
+    public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+        getDelegate().preRemove(client, protocolMapper);
+    }
 }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
index 2087fc2..97b62fc 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
@@ -1,6 +1,7 @@
 package org.keycloak.models.cache;
 
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleContainerModel;
@@ -274,4 +275,36 @@ public class UserAdapter implements UserModel {
         getDelegateForUpdate();
         updated.deleteRoleMapping(role);
     }
+
+    @Override
+    public GrantedConsentModel addGrantedConsent(GrantedConsentModel consent) {
+        getDelegateForUpdate();
+        return updated.addGrantedConsent(consent);
+    }
+
+    @Override
+    public GrantedConsentModel getGrantedConsentByClient(String clientId) {
+        // TODO: caching?
+        getDelegateForUpdate();
+        return updated.getGrantedConsentByClient(clientId);
+    }
+
+    @Override
+    public List<GrantedConsentModel> getGrantedConsents() {
+        // TODO: caching?
+        getDelegateForUpdate();
+        return updated.getGrantedConsents();
+    }
+
+    @Override
+    public void updateGrantedConsent(GrantedConsentModel consent) {
+        getDelegateForUpdate();
+        updated.updateGrantedConsent(consent);
+    }
+
+    @Override
+    public boolean revokeGrantedConsentForClient(String clientId) {
+        getDelegateForUpdate();
+        return updated.revokeGrantedConsentForClient(clientId);
+    }
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
index 7ebb242..498db9a 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
@@ -418,6 +418,8 @@ public class ClientAdapter implements ClientModel {
     public void removeProtocolMapper(ProtocolMapperModel mapping) {
         ProtocolMapperEntity toDelete = getProtocolMapperEntity(mapping.getId());
         if (toDelete != null) {
+            session.users().preRemove(this, mapping);
+
             this.entity.getProtocolMappers().remove(toDelete);
             em.remove(toDelete);
         }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentEntity.java
new file mode 100644
index 0000000..262f37f
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentEntity.java
@@ -0,0 +1,96 @@
+package org.keycloak.models.jpa.entities;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.persistence.CascadeType;
+import javax.persistence.CollectionTable;
+import javax.persistence.Column;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Entity
+@Table(name="GRANTED_CONSENT", uniqueConstraints = {
+        @UniqueConstraint(columnNames = {"USER_ID", "CLIENT_ID"})
+})
+@NamedQueries({
+        @NamedQuery(name="grantedConsentByUserAndClient", query="select consent from GrantedConsentEntity consent where consent.user.id = :userId and consent.clientId = :clientId"),
+        @NamedQuery(name="grantedConsentsByUser", query="select consent from GrantedConsentEntity consent where consent.user.id = :userId"),
+        @NamedQuery(name="deleteGrantedConsentsByRealm", query="delete from GrantedConsentEntity consent where consent.user IN (select user from UserEntity user where user.realmId = :realmId)"),
+        @NamedQuery(name="deleteGrantedConsentsByUser", query="delete from GrantedConsentEntity consent where consent.user = :user"),
+        @NamedQuery(name="deleteGrantedConsentsByClient", query="delete from GrantedConsentEntity consent where consent.clientId = :clientId"),
+})
+public class GrantedConsentEntity {
+
+    @Id
+    @Column(name="ID", length = 36)
+    protected String id;
+
+    @ManyToOne(fetch= FetchType.LAZY)
+    @JoinColumn(name="USER_ID")
+    protected UserEntity user;
+
+    @Column(name="CLIENT_ID")
+    protected String clientId;
+
+    @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "grantedConsent")
+    Collection<GrantedConsentRoleEntity> grantedRoles = new ArrayList<GrantedConsentRoleEntity>();
+
+    @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "grantedConsent")
+    Collection<GrantedConsentProtocolMapperEntity> grantedProtocolMappers = new ArrayList<GrantedConsentProtocolMapperEntity>();
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public UserEntity getUser() {
+        return user;
+    }
+
+    public void setUser(UserEntity user) {
+        this.user = user;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public Collection<GrantedConsentRoleEntity> getGrantedRoles() {
+        return grantedRoles;
+    }
+
+    public void setGrantedRoles(Collection<GrantedConsentRoleEntity> grantedRoles) {
+        this.grantedRoles = grantedRoles;
+    }
+
+    public Collection<GrantedConsentProtocolMapperEntity> getGrantedProtocolMappers() {
+        return grantedProtocolMappers;
+    }
+
+    public void setGrantedProtocolMappers(Collection<GrantedConsentProtocolMapperEntity> grantedProtocolMappers) {
+        this.grantedProtocolMappers = grantedProtocolMappers;
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentProtocolMapperEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentProtocolMapperEntity.java
new file mode 100644
index 0000000..7683dec
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentProtocolMapperEntity.java
@@ -0,0 +1,115 @@
+package org.keycloak.models.jpa.entities;
+
+import java.io.Serializable;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NamedQueries({
+        @NamedQuery(name="deleteGrantedConsentProtMappersByRealm", query=
+                "delete from GrantedConsentProtocolMapperEntity csm where csm.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.user IN (select user from UserEntity user where user.realmId = :realmId))"),
+        @NamedQuery(name="deleteGrantedConsentProtMappersByUser", query="delete from GrantedConsentProtocolMapperEntity csm where csm.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.user = :user)"),
+        @NamedQuery(name="deleteGrantedConsentProtMappersByProtocolMapper", query="delete from GrantedConsentProtocolMapperEntity csm where csm.protocolMapperId = :protocolMapperId)"),
+        @NamedQuery(name="deleteGrantedConsentProtMappersByClient", query="delete from GrantedConsentProtocolMapperEntity csm where csm.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.clientId = :clientId))"),
+})
+@Entity
+@Table(name="GRANTED_CONSENT_PROT_MAPPER")
+@IdClass(GrantedConsentProtocolMapperEntity.Key.class)
+public class GrantedConsentProtocolMapperEntity {
+
+    @Id
+    @ManyToOne(fetch= FetchType.LAZY)
+    @JoinColumn(name = "GRANTED_CONSENT_ID")
+    protected GrantedConsentEntity grantedConsent;
+
+    @Id
+    @Column(name="PROTOCOL_MAPPER_ID")
+    protected String protocolMapperId;
+
+    public GrantedConsentEntity getGrantedConsent() {
+        return grantedConsent;
+    }
+
+    public void setGrantedConsent(GrantedConsentEntity grantedConsent) {
+        this.grantedConsent = grantedConsent;
+    }
+
+    public String getProtocolMapperId() {
+        return protocolMapperId;
+    }
+
+    public void setProtocolMapperId(String protocolMapperId) {
+        this.protocolMapperId = protocolMapperId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        GrantedConsentProtocolMapperEntity that = (GrantedConsentProtocolMapperEntity)o;
+        Key myKey = new Key(this.grantedConsent, this.protocolMapperId);
+        Key hisKey = new Key(that.grantedConsent, that.protocolMapperId);
+        return myKey.equals(hisKey);
+    }
+
+    @Override
+    public int hashCode() {
+        Key myKey = new Key(this.grantedConsent, this.protocolMapperId);
+        return myKey.hashCode();
+    }
+
+    public static class Key implements Serializable {
+
+        protected GrantedConsentEntity grantedConsent;
+
+        protected String protocolMapperId;
+
+        public Key() {
+        }
+
+        public Key(GrantedConsentEntity grantedConsent, String protocolMapperId) {
+            this.grantedConsent = grantedConsent;
+            this.protocolMapperId = protocolMapperId;
+        }
+
+        public GrantedConsentEntity getGrantedConsent() {
+            return grantedConsent;
+        }
+
+        public String getProtocolMapperId() {
+            return protocolMapperId;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Key key = (Key) o;
+
+            if (grantedConsent != null ? !grantedConsent.getId().equals(key.grantedConsent != null ? key.grantedConsent.getId() : null) : key.grantedConsent != null) return false;
+            if (protocolMapperId != null ? !protocolMapperId.equals(key.protocolMapperId) : key.protocolMapperId != null) return false;
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = grantedConsent != null ? grantedConsent.getId().hashCode() : 0;
+            result = 31 * result + (protocolMapperId != null ? protocolMapperId.hashCode() : 0);
+            return result;
+        }
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentRoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentRoleEntity.java
new file mode 100644
index 0000000..ee2b3bb
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GrantedConsentRoleEntity.java
@@ -0,0 +1,115 @@
+package org.keycloak.models.jpa.entities;
+
+import java.io.Serializable;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NamedQueries({
+        @NamedQuery(name="deleteGrantedConsentRolesByRealm", query="delete from GrantedConsentRoleEntity grantedRole where grantedRole.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.user IN (select user from UserEntity user where user.realmId = :realmId))"),
+        @NamedQuery(name="deleteGrantedConsentRolesByUser", query="delete from GrantedConsentRoleEntity grantedRole where grantedRole.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.user = :user)"),
+        @NamedQuery(name="deleteGrantedConsentRolesByRole", query="delete from GrantedConsentRoleEntity grantedRole where grantedRole.roleId = :roleId)"),
+        @NamedQuery(name="deleteGrantedConsentRolesByClient", query="delete from GrantedConsentRoleEntity grantedRole where grantedRole.grantedConsent IN (select consent from GrantedConsentEntity consent where consent.clientId = :clientId)"),
+})
+@Entity
+@Table(name="GRANTED_CONSENT_ROLE")
+@IdClass(GrantedConsentRoleEntity.Key.class)
+public class GrantedConsentRoleEntity {
+
+    @Id
+    @ManyToOne(fetch= FetchType.LAZY)
+    @JoinColumn(name = "GRANTED_CONSENT_ID")
+    protected GrantedConsentEntity grantedConsent;
+
+    @Id
+    @Column(name="ROLE_ID")
+    protected String roleId;
+
+    public GrantedConsentEntity getGrantedConsent() {
+        return grantedConsent;
+    }
+
+    public void setGrantedConsent(GrantedConsentEntity grantedConsent) {
+        this.grantedConsent = grantedConsent;
+    }
+
+    public String getRoleId() {
+        return roleId;
+    }
+
+    public void setRoleId(String roleId) {
+        this.roleId = roleId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        GrantedConsentRoleEntity that = (GrantedConsentRoleEntity)o;
+        Key myKey = new Key(this.grantedConsent, this.roleId);
+        Key hisKey = new Key(that.grantedConsent, that.roleId);
+        return myKey.equals(hisKey);
+    }
+
+    @Override
+    public int hashCode() {
+        Key myKey = new Key(this.grantedConsent, this.roleId);
+        return myKey.hashCode();
+    }
+
+    public static class Key implements Serializable {
+
+        protected GrantedConsentEntity grantedConsent;
+
+        protected String roleId;
+
+        public Key() {
+        }
+
+        public Key(GrantedConsentEntity grantedConsent, String roleId) {
+            this.grantedConsent = grantedConsent;
+            this.roleId = roleId;
+        }
+
+        public GrantedConsentEntity getGrantedConsent() {
+            return grantedConsent;
+        }
+
+        public String getRoleId() {
+            return roleId;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Key key = (Key) o;
+
+            if (grantedConsent != null ? !grantedConsent.getId().equals(key.grantedConsent != null ? key.grantedConsent.getId() : null) : key.grantedConsent != null) return false;
+            if (roleId != null ? !roleId.equals(key.roleId) : key.roleId != null) return false;
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = grantedConsent != null ? grantedConsent.getId().hashCode() : 0;
+            result = 31 * result + (roleId != null ? roleId.hashCode() : 0);
+            return result;
+        }
+    }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 07d3f6f..15300d0 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -4,6 +4,7 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.CredentialValidationOutput;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialModel;
@@ -87,6 +88,9 @@ public class JpaUserProvider implements UserProvider {
     private void removeUser(UserEntity user) {
         em.createNamedQuery("deleteUserRoleMappingsByUser").setParameter("user", user).executeUpdate();
         em.createNamedQuery("deleteFederatedIdentityByUser").setParameter("user", user).executeUpdate();
+        em.createNamedQuery("deleteGrantedConsentRolesByUser").setParameter("user", user).executeUpdate();
+        em.createNamedQuery("deleteGrantedConsentProtMappersByUser").setParameter("user", user).executeUpdate();
+        em.createNamedQuery("deleteGrantedConsentsByUser").setParameter("user", user).executeUpdate();
         em.remove(user);
     }
 
@@ -130,7 +134,13 @@ public class JpaUserProvider implements UserProvider {
 
     @Override
     public void preRemove(RealmModel realm) {
-        int num = em.createNamedQuery("deleteUserRoleMappingsByRealm")
+        int num = em.createNamedQuery("deleteGrantedConsentRolesByRealm")
+                .setParameter("realmId", realm.getId()).executeUpdate();
+        num = em.createNamedQuery("deleteGrantedConsentProtMappersByRealm")
+                .setParameter("realmId", realm.getId()).executeUpdate();
+        num = em.createNamedQuery("deleteGrantedConsentsByRealm")
+                .setParameter("realmId", realm.getId()).executeUpdate();
+        num = em.createNamedQuery("deleteUserRoleMappingsByRealm")
                 .setParameter("realmId", realm.getId()).executeUpdate();
         num = em.createNamedQuery("deleteUserRequiredActionsByRealm")
                 .setParameter("realmId", realm.getId()).executeUpdate();
@@ -174,10 +184,25 @@ public class JpaUserProvider implements UserProvider {
 
     @Override
     public void preRemove(RealmModel realm, RoleModel role) {
+        em.createNamedQuery("deleteGrantedConsentRolesByRole").setParameter("roleId", role.getId()).executeUpdate();
         em.createNamedQuery("deleteUserRoleMappingsByRole").setParameter("roleId", role.getId()).executeUpdate();
     }
 
     @Override
+    public void preRemove(RealmModel realm, ClientModel client) {
+        em.createNamedQuery("deleteGrantedConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate();
+        em.createNamedQuery("deleteGrantedConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate();
+        em.createNamedQuery("deleteGrantedConsentsByClient").setParameter("clientId", client.getId()).executeUpdate();
+    }
+
+    @Override
+    public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+        em.createNamedQuery("deleteGrantedConsentProtMappersByProtocolMapper")
+                .setParameter("protocolMapperId", protocolMapper.getId())
+                .executeUpdate();
+    }
+
+    @Override
     public UserModel getUserById(String id, RealmModel realm) {
         TypedQuery<UserEntity> query = em.createNamedQuery("getRealmUserById", UserEntity.class);
         query.setParameter("id", id);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 2ab9ac7..4cc5ddf 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -658,6 +658,8 @@ public class RealmAdapter implements RealmModel {
         ClientModel client = getClientById(id);
         if (client == null) return false;
 
+        session.users().preRemove(this, client);
+
         for (RoleModel role : client.getRoles()) {
             client.removeRole(role);
         }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index 22796a4..f8d9352 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -1,6 +1,9 @@
 package org.keycloak.models.jpa;
 
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleContainerModel;
@@ -9,6 +12,9 @@ import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.jpa.entities.CredentialEntity;
+import org.keycloak.models.jpa.entities.GrantedConsentEntity;
+import org.keycloak.models.jpa.entities.GrantedConsentProtocolMapperEntity;
+import org.keycloak.models.jpa.entities.GrantedConsentRoleEntity;
 import org.keycloak.models.jpa.entities.UserAttributeEntity;
 import org.keycloak.models.jpa.entities.UserEntity;
 import org.keycloak.models.jpa.entities.UserRequiredActionEntity;
@@ -24,6 +30,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -473,6 +480,164 @@ public class UserAdapter implements UserModel {
     }
 
     @Override
+    public GrantedConsentModel addGrantedConsent(GrantedConsentModel consent) {
+        String clientId = consent.getClientId();
+        if (clientId == null) {
+            throw new ModelException("clientId needs to be filled for newly added consent!");
+        }
+
+        GrantedConsentEntity consentEntity = getGrantedConsentEntity(clientId);
+        if (consentEntity != null) {
+            throw new ModelDuplicateException("Consent already exists for client [" + clientId + "] and user [" + user.getId() + "]");
+        }
+
+        consentEntity = new GrantedConsentEntity();
+        consentEntity.setId(KeycloakModelUtils.generateId());
+        consentEntity.setUser(user);
+        consentEntity.setClientId(clientId);
+        em.persist(consentEntity);
+        em.flush();
+
+        updateGrantedConsentEntity(consentEntity, consent);
+
+        return consent;
+    }
+
+    @Override
+    public GrantedConsentModel getGrantedConsentByClient(String clientId) {
+        GrantedConsentEntity entity = getGrantedConsentEntity(clientId);
+        return toConsentModel(entity);
+    }
+
+    @Override
+    public List<GrantedConsentModel> getGrantedConsents() {
+        TypedQuery<GrantedConsentEntity> query = em.createNamedQuery("grantedConsentsByUser", GrantedConsentEntity.class);
+        query.setParameter("userId", getId());
+        List<GrantedConsentEntity> results = query.getResultList();
+
+        List<GrantedConsentModel> consents = new ArrayList<GrantedConsentModel>();
+        for (GrantedConsentEntity entity : results) {
+            GrantedConsentModel model = toConsentModel(entity);
+            consents.add(model);
+        }
+        return consents;
+    }
+
+    @Override
+    public void updateGrantedConsent(GrantedConsentModel consent) {
+        String clientId = consent.getClientId();
+        if (clientId == null) {
+            throw new ModelException("clientId needs to be for newly added consent!");
+        }
+
+        GrantedConsentEntity consentEntity = getGrantedConsentEntity(clientId);
+        if (consentEntity == null) {
+            throw new ModelException("Consent not found for client [" + clientId + "] and user [" + user.getId() + "]");
+        }
+
+        updateGrantedConsentEntity(consentEntity, consent);
+    }
+
+    @Override
+    public boolean revokeGrantedConsentForClient(String clientId) {
+        GrantedConsentEntity consentEntity = getGrantedConsentEntity(clientId);
+        if (consentEntity == null) return false;
+
+        em.remove(consentEntity);
+        em.flush();
+        return true;
+    }
+
+
+    private GrantedConsentEntity getGrantedConsentEntity(String clientId) {
+        TypedQuery<GrantedConsentEntity> query = em.createNamedQuery("grantedConsentByUserAndClient", GrantedConsentEntity.class);
+        query.setParameter("userId", getId());
+        query.setParameter("clientId", clientId);
+        List<GrantedConsentEntity> results = query.getResultList();
+        if (results.size() > 1) {
+            throw new ModelException("More results found for user [" + getUsername() + "] and client [" + clientId + "]");
+        } else if (results.size() == 1) {
+            return results.get(0);
+        } else {
+            return null;
+        }
+    }
+
+    private GrantedConsentModel toConsentModel(GrantedConsentEntity entity) {
+        if (entity == null) {
+            return null;
+        }
+
+        GrantedConsentModel model = new GrantedConsentModel(entity.getClientId());
+
+        Collection<GrantedConsentRoleEntity> grantedRoleEntities = entity.getGrantedRoles();
+        if (grantedRoleEntities != null) {
+            for (GrantedConsentRoleEntity grantedRole : grantedRoleEntities) {
+                model.addGrantedRole(grantedRole.getRoleId());
+            }
+        }
+
+        Collection<GrantedConsentProtocolMapperEntity> grantedProtocolMapperEntities = entity.getGrantedProtocolMappers();
+        if (grantedProtocolMapperEntities != null) {
+            for (GrantedConsentProtocolMapperEntity grantedProtMapper : grantedProtocolMapperEntities) {
+                model.addGrantedProtocolMapper(grantedProtMapper.getProtocolMapperId());
+            }
+        }
+
+        return model;
+    }
+
+    // Update roles and protocolMappers to given consentEntity from the consentModel
+    private void updateGrantedConsentEntity(GrantedConsentEntity consentEntity, GrantedConsentModel consentModel) {
+        Collection<GrantedConsentProtocolMapperEntity> grantedProtocolMapperEntities = consentEntity.getGrantedProtocolMappers();
+        Collection<GrantedConsentProtocolMapperEntity> mappersToRemove = new HashSet<GrantedConsentProtocolMapperEntity>(grantedProtocolMapperEntities);
+
+        for (String protocolMapperId : consentModel.getGrantedProtocolMappers()) {
+            GrantedConsentProtocolMapperEntity grantedProtocolMapperEntity = new GrantedConsentProtocolMapperEntity();
+            grantedProtocolMapperEntity.setGrantedConsent(consentEntity);
+            grantedProtocolMapperEntity.setProtocolMapperId(protocolMapperId);
+
+            // Check if it's already there
+            if (!grantedProtocolMapperEntities.contains(grantedProtocolMapperEntity)) {
+                em.persist(grantedProtocolMapperEntity);
+                em.flush();
+                grantedProtocolMapperEntities.add(grantedProtocolMapperEntity);
+            } else {
+                mappersToRemove.remove(grantedProtocolMapperEntity);
+            }
+        }
+        // Those mappers were no longer on consentModel and will be removed
+        for (GrantedConsentProtocolMapperEntity toRemove : mappersToRemove) {
+            grantedProtocolMapperEntities.remove(toRemove);
+            em.remove(toRemove);
+        }
+
+        Collection<GrantedConsentRoleEntity> grantedRoleEntities = consentEntity.getGrantedRoles();
+        Set<GrantedConsentRoleEntity> rolesToRemove = new HashSet<GrantedConsentRoleEntity>(grantedRoleEntities);
+        for (String roleId : consentModel.getGrantedRoles()) {
+            GrantedConsentRoleEntity consentRoleEntity = new GrantedConsentRoleEntity();
+            consentRoleEntity.setGrantedConsent(consentEntity);
+            consentRoleEntity.setRoleId(roleId);
+
+            // Check if it's already there
+            if (!grantedRoleEntities.contains(consentRoleEntity)) {
+                em.persist(consentRoleEntity);
+                em.flush();
+                grantedRoleEntities.add(consentRoleEntity);
+            } else {
+                rolesToRemove.remove(consentRoleEntity);
+            }
+        }
+        // Those roles were no longer on consentModel and will be removed
+        for (GrantedConsentRoleEntity toRemove : rolesToRemove) {
+            grantedRoleEntities.remove(toRemove);
+            em.remove(toRemove);
+        }
+
+        em.flush();
+    }
+
+    @Override
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || !(o instanceof UserModel)) return false;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
index 803cedf..8e07bc9 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
@@ -321,6 +321,8 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
     public void removeProtocolMapper(ProtocolMapperModel mapping) {
         for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) {
             if (entity.getId().equals(mapping.getId())) {
+                session.users().preRemove(this, mapping);
+
                 getMongoEntity().getProtocolMappers().remove(entity);
                 updateMongoEntity();
                 break;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 862e387..f71c797 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -9,6 +9,7 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.CredentialValidationOutput;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialModel;
@@ -352,6 +353,16 @@ public class MongoUserProvider implements UserProvider {
     }
 
     @Override
+    public void preRemove(RealmModel realm, ClientModel client) {
+        // TODO
+    }
+
+    @Override
+    public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+        // TODO
+    }
+
+    @Override
     public void preRemove(RealmModel realm, RoleModel role) {
         // todo not sure what to do for this
     }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index e515dde..27b62f6 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -639,6 +639,12 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
 
     @Override
     public boolean removeClient(String id) {
+        if (id == null) return false;
+        ClientModel client = getClientById(id);
+        if (client == null) return false;
+
+        session.users().preRemove(this, client);
+
         return getMongoStore().removeEntity(MongoClientEntity.class, id, invocationContext);
     }
 
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index 4d3c772..5fa3e52 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -4,6 +4,7 @@ import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
 
 import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
@@ -421,6 +422,35 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
     }
 
     @Override
+    public GrantedConsentModel addGrantedConsent(GrantedConsentModel consent) {
+        // TODO
+        return null;
+    }
+
+    @Override
+    public GrantedConsentModel getGrantedConsentByClient(String clientId) {
+        // TODO
+        return null;
+    }
+
+    @Override
+    public List<GrantedConsentModel> getGrantedConsents() {
+        // TODO
+        return null;
+    }
+
+    @Override
+    public void updateGrantedConsent(GrantedConsentModel consent) {
+        // TODO
+    }
+
+    @Override
+    public boolean revokeGrantedConsentForClient(String clientId) {
+        // TODO
+        return false;
+    }
+
+    @Override
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || !(o instanceof UserModel)) return false;
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index b146e8a..67e7570 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -14,7 +14,9 @@ import org.keycloak.jose.jws.JWSBuilder;
 import org.keycloak.login.LoginFormsProvider;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.GrantedConsentModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredCredentialModel;
 import org.keycloak.models.RoleModel;
@@ -418,9 +420,17 @@ public class AuthenticationManager {
         if (client.isConsentRequired()) {
             accessCode.setAction(ClientSessionModel.Action.OAUTH_GRANT);
 
+            GrantedConsentModel grantedConsent = user.getGrantedConsentByClient(client.getId());
+
             List<RoleModel> realmRoles = new LinkedList<RoleModel>();
             MultivaluedMap<String, RoleModel> resourceRoles = new MultivaluedMapImpl<String, RoleModel>();
             for (RoleModel r : accessCode.getRequestedRoles()) {
+
+                // Consent already granted by user
+                if (grantedConsent != null && grantedConsent.getGrantedRoles().contains(r.getId())) {
+                    continue;
+                }
+
                 if (r.getContainer() instanceof RealmModel) {
                     realmRoles.add(r);
                 } else {
@@ -428,10 +438,22 @@ public class AuthenticationManager {
                 }
             }
 
-            return session.getProvider(LoginFormsProvider.class)
-                    .setClientSessionCode(accessCode.getCode())
-                    .setAccessRequest(realmRoles, resourceRoles)
-                    .createOAuthGrant(clientSession);
+            List<ProtocolMapperModel> protocolMappers = new LinkedList<ProtocolMapperModel>();
+            for (ProtocolMapperModel model : client.getProtocolMappers()) {
+                if (model.isConsentRequired() && model.getProtocol().equals(clientSession.getAuthMethod()) && model.getConsentText() != null) {
+                    if (grantedConsent == null || !grantedConsent.getGrantedProtocolMappers().contains(model.getId())) {
+                        protocolMappers.add(model);
+                    }
+                }
+            }
+
+            // Skip grant screen if everything was already approved by this user
+            if (realmRoles.size() > 0 || resourceRoles.size() > 0 || protocolMappers.size() > 0) {
+                return session.getProvider(LoginFormsProvider.class)
+                        .setClientSessionCode(accessCode.getCode())
+                        .setAccessRequest(realmRoles, resourceRoles, protocolMappers)
+                        .createOAuthGrant(clientSession);
+            }
         }
 
         event.success();
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index aa2e9ab..f142e9a 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -152,6 +152,8 @@ public class Messages {
 
     public static final String SUCCESS_TOTP = "successTotpMessage";
 
+    public static final String SUCCESS_GRANT_REVOKED = "successGrantRevokedMessage";
+
     public static final String MISSING_IDENTITY_PROVIDER = "missingIdentityProviderMessage";
 
     public static final String INVALID_FEDERATED_IDENTITY_ACTION = "invalidFederatedIdentityActionMessage";
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 3fb84c6..af1c872 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -41,6 +41,7 @@ import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.models.utils.TimeBasedOTP;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.protocol.oidc.utils.RedirectUtils;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
@@ -75,6 +76,7 @@ import javax.ws.rs.core.Variant;
 
 import java.lang.reflect.Method;
 import java.net.URI;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -348,6 +350,12 @@ public class AccountService {
         return forwardToPage("sessions", AccountPages.SESSIONS);
     }
 
+    @Path("access")
+    @GET
+    public Response accessPage() {
+        return forwardToPage("access", AccountPages.ACCESS);
+    }
+
     /**
      * Check to see if form post has sessionId hidden field and match it against the session id.
      *
@@ -483,6 +491,46 @@ public class AccountService {
         return Response.seeOther(location).build();
     }
 
+    @Path("revoke-grant")
+    @POST
+    public Response processRevokeGrant(final MultivaluedMap<String, String> formData) {
+        if (auth == null) {
+            return login("access");
+        }
+
+        require(AccountRoles.MANAGE_ACCOUNT);
+        csrfCheck(formData);
+
+        String clientId = formData.getFirst("clientId");
+        if (clientId == null) {
+            return account.setError(Messages.CLIENT_NOT_FOUND).createResponse(AccountPages.ACCESS);
+        }
+        ClientModel client = realm.getClientById(clientId);
+        if (client == null) {
+            return account.setError(Messages.CLIENT_NOT_FOUND).createResponse(AccountPages.ACCESS);
+        }
+
+        // Revoke grant in UserModel
+        UserModel user = auth.getUser();
+        user.revokeGrantedConsentForClient(client.getId());
+
+        // Logout clientSessions for this user and client
+        List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
+        for (UserSessionModel userSession : userSessions) {
+            List<ClientSessionModel> clientSessions = userSession.getClientSessions();
+            for (ClientSessionModel clientSession : clientSessions) {
+                if (clientSession.getClient().getId().equals(clientId)) {
+                    TokenManager.dettachClientSession(session.sessions(), realm, clientSession);
+                }
+            }
+        }
+
+        event.event(EventType.REVOKE_GRANT).client(auth.getClient()).user(auth.getUser()).detail(Details.REVOKED_CLIENT, client.getClientId()).success();
+        setReferrerOnPage();
+
+        return account.setSuccess(Messages.SUCCESS_GRANT_REVOKED).createResponse(AccountPages.ACCESS);
+    }
+
     /**
      * Update the TOTP for this account.
      *
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 1a8be49..a0eb4dc 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -34,8 +34,10 @@ import org.keycloak.jose.jws.JWSBuilder;
 import org.keycloak.login.LoginFormsProvider;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.GrantedConsentModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelException;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredCredentialModel;
 import org.keycloak.models.UserCredentialModel;
@@ -576,19 +578,19 @@ public class LoginActionsService {
         event.detail(Details.CODE_ID, clientSession.getId());
 
         String redirect = clientSession.getRedirectUri();
+        UserSessionModel userSession = clientSession.getUserSession();
+        UserModel user = userSession.getUser();
+        ClientModel client = clientSession.getClient();
 
-        event.client(clientSession.getClient())
-                .user(clientSession.getUserSession().getUser())
+        event.client(client)
+                .user(user)
                 .detail(Details.RESPONSE_TYPE, "code")
                 .detail(Details.REDIRECT_URI, redirect);
 
-        UserSessionModel userSession = clientSession.getUserSession();
-        if (userSession != null) {
-            event.detail(Details.AUTH_METHOD, userSession.getAuthMethod());
-            event.detail(Details.USERNAME, userSession.getLoginUsername());
-            if (userSession.isRememberMe()) {
-                event.detail(Details.REMEMBER_ME, "true");
-            }
+        event.detail(Details.AUTH_METHOD, userSession.getAuthMethod());
+        event.detail(Details.USERNAME, userSession.getLoginUsername());
+        if (userSession.isRememberMe()) {
+            event.detail(Details.REMEMBER_ME, "true");
         }
 
         if (!AuthenticationManager.isSessionValid(realm, userSession)) {
@@ -607,6 +609,21 @@ public class LoginActionsService {
             return protocol.consentDenied(clientSession);
         }
 
+        GrantedConsentModel grantedConsent = user.getGrantedConsentByClient(client.getId());
+        if (grantedConsent == null) {
+            grantedConsent = user.addGrantedConsent(new GrantedConsentModel(client.getId()));
+        }
+        for (String roleId : clientSession.getRoles()) {
+            grantedConsent.addGrantedRole(roleId);
+        }
+        // TODO: It's not 100% sure that approved protocolMappers are same like the protocolMappers retrieved here from the client. Maybe clientSession.setProtocolMappers/getProtocolMappers should be added...
+        for (ProtocolMapperModel protocolMapper : client.getProtocolMappers()) {
+            if (protocolMapper.isConsentRequired() && protocolMapper.getProtocol().equals(clientSession.getAuthMethod()) && protocolMapper.getConsentText() != null) {
+                grantedConsent.addGrantedProtocolMapper(protocolMapper.getId());
+            }
+        }
+        user.updateGrantedConsent(grantedConsent);
+
         event.success();
 
         return authManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection);
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index a8cb811..0d3cc32 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -118,6 +118,11 @@ public class Urls {
                 .build(realmId);
     }
 
+    public static URI accountRevokeClientPage(URI baseUri, String realmId) {
+        return accountBase(baseUri).path(AccountService.class, "processRevokeGrant")
+                .build(realmId);
+    }
+
     public static URI accountLogout(URI baseUri, URI redirectUri, String realmId) {
         return realmLogout(baseUri).queryParam("redirect_uri", redirectUri).build(realmId);
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
index 44ebaf1..73f7655 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
@@ -21,6 +21,7 @@ import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.resources.RealmsResource;
 import org.keycloak.testsuite.Constants;
 import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AccountAccessPage;
 import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
 import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.pages.OAuthGrantPage;
@@ -88,6 +89,9 @@ public class ProfileTest {
     protected AccountUpdateProfilePage profilePage;
 
     @WebResource
+    protected AccountAccessPage accountAccessPage;
+
+    @WebResource
     protected LoginPage loginPage;
 
     @WebResource
@@ -186,6 +190,9 @@ public class ProfileTest {
         JSONObject profile = new JSONObject(IOUtils.toString(response.getEntity().getContent()));
 
         assertEquals("test-user@localhost", profile.getString("username"));
+
+        accountAccessPage.open();
+        accountAccessPage.revokeGrant("third-party");
     }
 
     @Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
index ea06aac..e09624c 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
@@ -89,7 +89,7 @@ public abstract class AbstractIdentityProviderTest {
     public WebRule webRule = new WebRule(this);
 
     @WebResource
-    private WebDriver driver;
+    protected WebDriver driver;
 
     @WebResource
     private LoginPage loginPage;
@@ -122,6 +122,7 @@ public abstract class AbstractIdentityProviderTest {
 
     @After
     public void onAfter() {
+        revokeGrant();
         brokerServerRule.stopSession(this.session, true);
     }
 
@@ -387,6 +388,9 @@ public abstract class AbstractIdentityProviderTest {
         assertTrue(accountFederatedIdentityPage.isCurrent());
         assertTrue(driver.getPageSource().contains("id=\"remove-" + identityProviderModel.getAlias() + "\""));
 
+        // Revoke grant in account mgmt
+        revokeGrant();
+
         // Logout from account management
         accountFederatedIdentityPage.logout();
         assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
@@ -401,6 +405,9 @@ public abstract class AbstractIdentityProviderTest {
         accountFederatedIdentityPage.clickRemoveProvider(identityProviderModel.getAlias());
         assertTrue(driver.getPageSource().contains("id=\"add-" + identityProviderModel.getAlias() + "\""));
 
+        // Revoke grant in account mgmt
+        revokeGrant();
+
         // Logout from account management
         accountFederatedIdentityPage.logout();
         assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
@@ -637,6 +644,10 @@ public abstract class AbstractIdentityProviderTest {
 
     }
 
+    protected void revokeGrant() {
+
+    }
+
     protected abstract String getProviderId();
 
     protected IdentityProviderModel getIdentityProviderModel() {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
index 3a79a40..87c1b3b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
@@ -1,19 +1,26 @@
 package org.keycloak.testsuite.broker;
 
+import org.junit.After;
 import org.junit.ClassRule;
 import org.junit.Test;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.services.Urls;
 import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.Constants;
+import org.keycloak.testsuite.pages.AccountAccessPage;
 import org.keycloak.testsuite.pages.OAuthGrantPage;
 import org.keycloak.testsuite.rule.AbstractKeycloakRule;
 import org.keycloak.testsuite.rule.WebResource;
 import org.keycloak.testutils.KeycloakServer;
 import org.keycloak.util.JsonSerialization;
+import org.openqa.selenium.NoSuchElementException;
 
 import java.io.IOException;
 
+import javax.ws.rs.core.UriBuilder;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
@@ -22,12 +29,14 @@ import static org.junit.Assert.fail;
  */
 public class OIDCKeyCloakServerBrokerBasicTest extends AbstractIdentityProviderTest {
 
+    private static final int PORT = 8082;
+
     @ClassRule
     public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() {
 
         @Override
         protected void configureServer(KeycloakServer server) {
-            server.getConfig().setPort(8082);
+            server.getConfig().setPort(PORT);
         }
 
         @Override
@@ -44,6 +53,25 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractIdentityProviderT
     @WebResource
     private OAuthGrantPage grantPage;
 
+    @WebResource
+    protected AccountAccessPage accountAccessPage;
+
+    @Override
+    protected void revokeGrant() {
+        String currentUrl = driver.getCurrentUrl();
+
+        String accountAccessPath = Urls.accountAccessPage(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).port(PORT).build(), "realm-with-oidc-identity-provider").toString();
+        accountAccessPage.setPath(accountAccessPath);
+        accountAccessPage.open();
+        try {
+            accountAccessPage.revokeGrant("broker-app");
+        } catch (NoSuchElementException e) {
+            System.err.println("Couldn't revoke broker-app application, maybe because it wasn't granted or user not logged");
+        }
+
+        driver.navigate().to(currentUrl);
+    }
+
     @Override
     protected void doAfterProviderAuthentication() {
         // grant access to broker-app
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GrantedConsentModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GrantedConsentModelTest.java
new file mode 100644
index 0000000..5c555b7
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GrantedConsentModelTest.java
@@ -0,0 +1,266 @@
+package org.keycloak.testsuite.model;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.GrantedConsentModel;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GrantedConsentModelTest extends AbstractModelTest {
+
+    @Before
+    public void setupEnv() {
+        RealmModel realm = realmManager.createRealm("original");
+
+        ClientModel fooClient = realm.addClient("foo-client");
+        ClientModel barClient = realm.addClient("bar-client");
+
+        RoleModel realmRole = realm.addRole("realm-role");
+        RoleModel barClientRole = barClient.addRole("bar-client-role");
+
+        ProtocolMapperModel fooMapper = new ProtocolMapperModel();
+        fooMapper.setName("foo");
+        fooMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        fooMapper.setProtocolMapper(UserPropertyMapper.PROVIDER_ID);
+        fooMapper = fooClient.addProtocolMapper(fooMapper);
+
+        ProtocolMapperModel barMapper = new ProtocolMapperModel();
+        barMapper.setName("bar");
+        barMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        barMapper.setProtocolMapper(UserPropertyMapper.PROVIDER_ID);
+        barMapper = barClient.addProtocolMapper(barMapper);
+
+        UserModel john = session.users().addUser(realm, "john");
+        UserModel mary = session.users().addUser(realm, "mary");
+
+        GrantedConsentModel johnFooGrant = new GrantedConsentModel(fooClient.getId());
+        johnFooGrant.addGrantedRole(realmRole.getId());
+        johnFooGrant.addGrantedRole(barClientRole.getId());
+        johnFooGrant.addGrantedProtocolMapper(fooMapper.getId());
+        john.addGrantedConsent(johnFooGrant);
+
+        GrantedConsentModel johnBarGrant = new GrantedConsentModel(barClient.getId());
+        johnBarGrant.addGrantedProtocolMapper(barMapper.getId());
+        johnBarGrant.addGrantedRole(realmRole.getId());
+
+        // Update should fail as grant doesn't yet exists
+        try {
+            john.updateGrantedConsent(johnBarGrant);
+            Assert.fail("Not expected to end here");
+        } catch (ModelException expected) {
+        }
+
+        john.addGrantedConsent(johnBarGrant);
+
+        GrantedConsentModel maryFooGrant = new GrantedConsentModel(fooClient.getId());
+        maryFooGrant.addGrantedRole(realmRole.getId());
+        maryFooGrant.addGrantedProtocolMapper(fooMapper.getId());
+        mary.addGrantedConsent(maryFooGrant);
+
+        commit();
+    }
+
+    @Test
+    public void basicConsentTest() {
+        RealmModel realm = realmManager.getRealm("original");
+        Map<String, ClientModel> clients = realm.getClientNameMap();
+        ClientModel fooClient = clients.get("foo-client");
+        ClientModel barClient = clients.get("bar-client");
+
+        UserModel john = session.users().getUserByUsername("john", realm);
+        UserModel mary = session.users().getUserByUsername("mary", realm);
+
+        GrantedConsentModel johnFooConsent = john.getGrantedConsentByClient(fooClient.getId());
+        Assert.assertEquals(johnFooConsent.getGrantedRoles().size(), 2);
+        Assert.assertEquals(johnFooConsent.getGrantedProtocolMappers().size(), 1);
+        Assert.assertTrue(isRoleGranted(realm, "realm-role", johnFooConsent));
+        Assert.assertTrue(isRoleGranted(barClient, "bar-client-role", johnFooConsent));
+        Assert.assertTrue(isMapperGranted(fooClient, "foo", johnFooConsent));
+
+        GrantedConsentModel johnBarConsent = john.getGrantedConsentByClient(barClient.getId());
+        Assert.assertEquals(johnBarConsent.getGrantedRoles().size(), 1);
+        Assert.assertEquals(johnBarConsent.getGrantedProtocolMappers().size(), 1);
+        Assert.assertTrue(isRoleGranted(realm, "realm-role", johnBarConsent));
+        Assert.assertTrue(isMapperGranted(barClient, "bar", johnBarConsent));
+
+        GrantedConsentModel maryConsent = mary.getGrantedConsentByClient(fooClient.getId());
+        Assert.assertEquals(maryConsent.getGrantedRoles().size(), 1);
+        Assert.assertEquals(maryConsent.getGrantedProtocolMappers().size(), 1);
+        Assert.assertTrue(isRoleGranted(realm, "realm-role", maryConsent));
+        Assert.assertFalse(isRoleGranted(barClient, "bar-client-role", maryConsent));
+        Assert.assertTrue(isMapperGranted(fooClient, "foo", maryConsent));
+
+        Assert.assertNull(mary.getGrantedConsentByClient(barClient.getId()));
+    }
+
+    @Test
+    public void getAllConsentTest() {
+        RealmModel realm = realmManager.getRealm("original");
+        Map<String, ClientModel> clients = realm.getClientNameMap();
+        ClientModel fooClient = clients.get("foo-client");
+
+        UserModel john = session.users().getUserByUsername("john", realm);
+        UserModel mary = session.users().getUserByUsername("mary", realm);
+
+        List<GrantedConsentModel> johnConsents = john.getGrantedConsents();
+        Assert.assertEquals(2, johnConsents.size());
+
+        List<GrantedConsentModel> maryConsents = mary.getGrantedConsents();
+        Assert.assertEquals(1, maryConsents.size());
+        GrantedConsentModel maryConsent = maryConsents.get(0);
+        Assert.assertEquals(maryConsent.getClientId(), fooClient.getId());
+        Assert.assertEquals(maryConsent.getGrantedRoles().size(), 1);
+        Assert.assertEquals(maryConsent.getGrantedProtocolMappers().size(), 1);
+        Assert.assertTrue(isRoleGranted(realm, "realm-role", maryConsent));
+        Assert.assertTrue(isMapperGranted(fooClient, "foo", maryConsent));
+    }
+
+    @Test
+    public void updateWithRoleRemovalTest() {
+        RealmModel realm = realmManager.getRealm("original");
+        ClientModel fooClient = realm.getClientNameMap().get("foo-client");
+        UserModel john = session.users().getUserByUsername("john", realm);
+
+        GrantedConsentModel johnConsent = john.getGrantedConsentByClient(fooClient.getId());
+
+        // Remove foo protocol mapper from johnConsent
+        ProtocolMapperModel protMapperModel = fooClient.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, "foo");
+        johnConsent.getGrantedProtocolMappers().remove(protMapperModel.getId());
+
+        // Remove realm-role and add new-realm-role to johnConsent
+        RoleModel realmRole = realm.getRole("realm-role");
+        johnConsent.getGrantedRoles().remove(realmRole.getId());
+
+        RoleModel newRealmRole = realm.addRole("new-realm-role");
+        johnConsent.addGrantedRole(newRealmRole.getId());
+
+        john.updateGrantedConsent(johnConsent);
+
+        commit();
+
+        realm = realmManager.getRealm("original");
+        fooClient = realm.getClientNameMap().get("foo-client");
+        john = session.users().getUserByUsername("john", realm);
+        johnConsent = john.getGrantedConsentByClient(fooClient.getId());
+
+        Assert.assertEquals(johnConsent.getGrantedRoles().size(), 2);
+        Assert.assertEquals(johnConsent.getGrantedProtocolMappers().size(), 0);
+        Assert.assertFalse(isRoleGranted(realm, "realm-role", johnConsent));
+        Assert.assertTrue(isRoleGranted(realm, "new-realm-role", johnConsent));
+        Assert.assertFalse(isMapperGranted(fooClient, "foo", johnConsent));
+    }
+
+    @Test
+    public void revokeTest() {
+        RealmModel realm = realmManager.getRealm("original");
+        ClientModel fooClient = realm.getClientNameMap().get("foo-client");
+        UserModel john = session.users().getUserByUsername("john", realm);
+
+        john.revokeGrantedConsentForClient(fooClient.getId());
+
+        commit();
+
+        realm = realmManager.getRealm("original");
+        john = session.users().getUserByUsername("john", realm);
+        Assert.assertNull(john.getGrantedConsentByClient(fooClient.getId()));
+    }
+
+    @Test
+    public void deleteUserTest() {
+        // Validate user deleted without any referential constraint errors
+        RealmModel realm = realmManager.getRealm("original");
+        UserModel john = session.users().getUserByUsername("john", realm);
+        session.users().removeUser(realm, john);
+    }
+
+    @Test
+    public void deleteProtocolMapperTest() {
+        RealmModel realm = realmManager.getRealm("original");
+        ClientModel fooClient = realm.getClientNameMap().get("foo-client");
+        ProtocolMapperModel fooMapper = fooClient.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, "foo");
+        String fooMapperId = fooMapper.getId();
+        fooClient.removeProtocolMapper(fooMapper);
+
+        commit();
+
+        realm = realmManager.getRealm("original");
+        fooClient = realm.getClientNameMap().get("foo-client");
+        UserModel john = session.users().getUserByUsername("john", realm);
+        GrantedConsentModel johnConsent = john.getGrantedConsentByClient(fooClient.getId());
+
+        Assert.assertEquals(johnConsent.getGrantedRoles().size(), 2);
+        Assert.assertEquals(johnConsent.getGrantedProtocolMappers().size(), 0);
+        Assert.assertFalse(johnConsent.isProtocolMapperGranted(fooMapperId));
+    }
+
+    @Test
+    public void deleteRoleTest() {
+        RealmModel realm = realmManager.getRealm("original");
+        RoleModel realmRole = realm.getRole("realm-role");
+        String realmRoleId = realmRole.getId();
+        realm.removeRole(realmRole);
+
+        commit();
+
+        realm = realmManager.getRealm("original");
+        Map<String, ClientModel> clients = realm.getClientNameMap();
+        ClientModel fooClient = clients.get("foo-client");
+        ClientModel barClient = clients.get("bar-client");
+        UserModel john = session.users().getUserByUsername("john", realm);
+        GrantedConsentModel johnConsent = john.getGrantedConsentByClient(fooClient.getId());
+
+        Assert.assertEquals(johnConsent.getGrantedRoles().size(), 1);
+        Assert.assertEquals(johnConsent.getGrantedProtocolMappers().size(), 1);
+        Assert.assertFalse(johnConsent.isRoleGranted(realmRoleId));
+        Assert.assertTrue(isRoleGranted(barClient, "bar-client-role", johnConsent));
+    }
+
+    @Test
+    public void deleteClientTest() {
+        RealmModel realm = realmManager.getRealm("original");
+        Map<String, ClientModel> clients = realm.getClientNameMap();
+        ClientModel barClient = clients.get("bar-client");
+        realm.removeClient(barClient.getId());
+
+        commit();
+
+        realm = realmManager.getRealm("original");
+        clients = realm.getClientNameMap();
+        ClientModel fooClient = clients.get("foo-client");
+        Assert.assertNull(clients.get("bar-client"));
+
+        UserModel john = session.users().getUserByUsername("john", realm);
+
+        GrantedConsentModel johnFooConsent = john.getGrantedConsentByClient(fooClient.getId());
+        Assert.assertEquals(johnFooConsent.getGrantedRoles().size(), 1);
+        Assert.assertEquals(johnFooConsent.getGrantedProtocolMappers().size(), 1);
+        Assert.assertTrue(isRoleGranted(realm, "realm-role", johnFooConsent));
+        Assert.assertTrue(isMapperGranted(fooClient, "foo", johnFooConsent));
+
+        Assert.assertNull(john.getGrantedConsentByClient(barClient.getId()));
+    }
+
+    private boolean isRoleGranted(RoleContainerModel roleContainer, String roleName, GrantedConsentModel consentModel) {
+        RoleModel role = roleContainer.getRole(roleName);
+        return consentModel.isRoleGranted(role.getId());
+    }
+
+    private boolean isMapperGranted(ClientModel client, String protocolMapperName, GrantedConsentModel consentModel) {
+        ProtocolMapperModel protocolMapper = client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, protocolMapperName);
+        return consentModel.isProtocolMapperGranted(protocolMapper.getId());
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountAccessPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountAccessPage.java
new file mode 100644
index 0000000..99b591a
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountAccessPage.java
@@ -0,0 +1,98 @@
+package org.keycloak.testsuite.pages;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.core.UriBuilder;
+
+import org.keycloak.services.Urls;
+import org.keycloak.testsuite.Constants;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AccountAccessPage extends AbstractAccountPage {
+
+    private String path = Urls.accountAccessPage(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).build(), "test").toString();
+
+    @Override
+    public boolean isCurrent() {
+        return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/access");
+    }
+
+    @Override
+    public void open() {
+        driver.navigate().to(path);
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+
+    public void revokeGrant(String clientId) {
+        driver.findElement(By.id("revoke-" + clientId)).click();
+    }
+
+    public Map<String, ClientGrant> getClientGrants() {
+        Map<String, ClientGrant> table = new HashMap<String, ClientGrant>();
+        for (WebElement r : driver.findElements(By.tagName("tr"))) {
+            int count = 0;
+            ClientGrant currentGrant = null;
+
+            for (WebElement col : r.findElements(By.tagName("td"))) {
+                count++;
+                switch (count) {
+                    case 1:
+                        currentGrant = new ClientGrant();
+                        String clientId = col.getText();
+                        table.put(clientId, currentGrant);
+                        break;
+                    case 2:
+                        String protMappersStr = col.getText();
+                        String[] protMappers = protMappersStr.split(",");
+                        for (String protMapper : protMappers) {
+                            protMapper = protMapper.trim();
+                            currentGrant.addMapper(protMapper);
+                        }
+                        break;
+                    case 3:
+                        String rolesStr = col.getText();
+                        String[] roles = rolesStr.split(",");
+                        for (String role : roles) {
+                            role = role.trim();
+                            currentGrant.addRole(role);
+                        }
+                        break;
+                }
+            }
+        }
+        table.remove("Client");
+        return table;
+    }
+
+    public static class ClientGrant {
+
+        private final List<String> protocolMapperDescriptions = new ArrayList<String>();
+        private final List<String> roleDescriptions = new ArrayList<String>();
+
+        private void addMapper(String protocolMapper) {
+            protocolMapperDescriptions.add(protocolMapper);
+        }
+
+        private void addRole(String role) {
+            roleDescriptions.add(role);
+        }
+
+        public List<String> getProtocolMapperDescriptions() {
+            return protocolMapperDescriptions;
+        }
+
+        public List<String> getRoleDescriptions() {
+            return roleDescriptions;
+        }
+    }
+}
diff --git a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json
index 01a1c2c..cf0f508 100755
--- a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json
+++ b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json
@@ -32,7 +32,10 @@
                 { "type" : "password",
                     "value" : "password" }
             ],
-            "realmRoles": ["manager"]
+            "realmRoles": ["manager"],
+            "applicationRoles": {
+                "account": [ "manage-account" ]
+            }
         },
         {
             "username" : "test-user-noemail",
@@ -43,7 +46,10 @@
                 { "type" : "password",
                     "value" : "password" }
             ],
-            "realmRoles": ["manager"]
+            "realmRoles": ["manager"],
+            "applicationRoles": {
+                "account": [ "manage-account" ]
+            }
         },
         {
           "username" : "pedroigor",
@@ -53,7 +59,10 @@
             { "type" : "password",
               "value" : "password" }
           ],
-          "realmRoles": ["manager"]
+          "realmRoles": ["manager"],
+          "applicationRoles": {
+              "account": [ "manage-account" ]
+          }
         }
     ],
     "roles" : {