Repository: gravitee-io/graviteeio-access-management
Pull request #549 (2019-03-27 16:35:45)

feat(approval): add user consent management
closes gravitee-io/issues#1851


Patch:
diff --git a/docs/oauth2-api-descriptor.yml b/docs/oauth2-api-descriptor.yml
index b7cd1f3c71..98fa4d15cb 100644
--- a/docs/oauth2-api-descriptor.yml
+++ b/docs/oauth2-api-descriptor.yml
@@ -19,7 +19,7 @@ info:
   description: >-
     Gravitee.io - Access Management - OAuth 2.0 API.
     Defines The OAuth 2.0 Authorization Framework Endpoints exposed by AM server.
-  version: 2.2.0
+  version: 2.5.0
   title: Gravitee.io - Access Management - OAuth 2.0 API
   contact:
     email: contact@graviteesource.com
diff --git a/docs/oidc-api-descriptor.yml b/docs/oidc-api-descriptor.yml
index 4ed8fbe8c7..d208d155c1 100644
--- a/docs/oidc-api-descriptor.yml
+++ b/docs/oidc-api-descriptor.yml
@@ -19,7 +19,7 @@ info:
   description: >-
     Gravitee.io - Access Management - OpenID Connect API.
     Defines The OpenID Connect Endpoints exposed by AM server.
-  version: 2.2.0
+  version: 2.5.0
   title: Gravitee.io - Access Management - OpenID Connect API
   contact:
     email: contact@graviteesource.com
diff --git a/docs/scim-api-descriptor.yml b/docs/scim-api-descriptor.yml
index eb34769c78..d5b9ce4754 100644
--- a/docs/scim-api-descriptor.yml
+++ b/docs/scim-api-descriptor.yml
@@ -19,7 +19,7 @@ info:
   description: >-
     Gravitee.io - Access Management - SCIM 2.0 API.
     Defines The SCIM 2.0 Endpoints exposed by AM server.
-  version: 2.2.0
+  version: 2.5.0
   title: Gravitee.io - Access Management - SCIM 2.0 API
   contact:
     email: contact@graviteesource.com
diff --git a/docs/users-api-descriptor.yml b/docs/users-api-descriptor.yml
new file mode 100644
index 0000000000..fe3f158519
--- /dev/null
+++ b/docs/users-api-descriptor.yml
@@ -0,0 +1,145 @@
+#
+# Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+swagger: '2.0'
+info:
+  description: >-
+    Gravitee.io - Access Management - Users API.
+    Defines Users Endpoints exposed by AM server.
+
+    Users endpoints are OAuth 2.0 secured and can be access either with a token with "consent_admin" scope
+    or if the token 'sub' claim match the "userId" path parameter.
+  version: 2.5.0
+  title: Gravitee.io - Access Management - Users API
+  contact:
+    email: contact@graviteesource.com
+  license:
+    name: Apache 2.0
+    url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
+host: auth.gravitee.io
+basePath: /{domain}/users
+schemes:
+- https
+paths:
+  /{userId}/consents:
+    get:
+      tags:
+      - Users
+      summary: List user consents
+      description: Lists all consents for the specified user
+      produces:
+      - application/json
+      parameters:
+      - in: query
+        name: clientId
+        type: string
+        description: Specifies user-client consents combination
+        default: 1
+      responses:
+        '200':
+          description: A JSON object that represents a set of user consents
+          schema:
+            $ref: '#/definitions/UserConsents'
+        '401':
+          description: Invalid Token
+    delete:
+      tags:
+      - Users
+      summary: Revoke user consents
+      description: Revokes all consents for a specified user
+      parameters:
+      - in: query
+        name: clientId
+        description: Revoke user's consents per application basis
+      responses:
+        '204':
+          description: User's consents revoked
+        '401':
+          description: Invalid Token
+  /{userId}/consents/{consentId}:
+    get:
+      tags:
+      - Users
+      summary: Get user consent
+      description: Gets a consent for the specified user
+      produces:
+      - application/json
+      responses:
+        '200':
+          description: A JSON object that represents a user consent
+          schema:
+            $ref: '#/definitions/UserConsent'
+        '401':
+          description: Invalid Token
+        '404':
+          description: Consent not found
+    delete:
+      tags:
+      - Users
+      summary: Revoke user consent
+      description: Revokes one consent for a specified user
+      responses:
+        '204':
+          description: User consent is revoked
+        '401':
+          description: Invalid Token
+securityDefinitions:
+  bearerAuth:
+    type: oauth2
+    tokenUrl: https://auth.gravitee.io/{domain}/oauth/token
+    authorizationUrl: https://auth.gravitee.io/{domain}/authorize
+    scopes:
+      consent_admin: Grants consent_admin access
+definitions:
+  UserConsents:
+    type: object
+    properties:
+      schemas:
+        type: array
+        description: Array of Strings containing URIs that are used to indicate the namespaces of the SCIM schemas.
+        items:
+          $ref: '#/definitions/UserConsent'
+  UserConsent:
+    type: object
+    properties:
+      id:
+        type: string
+        description: Consent technical id.
+      userId:
+        type: string
+        description: User technical id.
+      clientId:
+        type: string
+        description: OAuth 2.0 client_id.
+      domain:
+        type: string
+        description: Security domain technical id.
+      status:
+        type: string
+        description: Consent status.
+        enum: [approved, denied]
+      scope:
+        type: string
+        description: Scope (permission) approved/denied by the user.
+      expiresAt:
+        type: date
+        description: Consent expiration date.
+      createadAt:
+        type: date
+        description: Consent creation date.
+      updatedAt:
+        type: date
+        description: Consent update date.
diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java
index c3f6ef9b5a..b775d2198c 100644
--- a/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java
+++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java
@@ -42,7 +42,8 @@ public enum Scope {
     OFFLINE_ACCESS("offline_access", "Offline_access","Access to End-User UserInfo even when he is not logged in.", Collections.emptyList()),
     DCR("dcr", "Client_registration", "Access to client information through openid register endpoint.", Collections.emptyList()),
     DCR_ADMIN("dcr_admin", "Client_registration_admin", "Access to Dynamic Client Registration endpoint.", Collections.emptyList()),
-    SCIM("scim", "SCIM", "Access to System for Cross-domain Identity Management endpoint.", Collections.emptyList());
+    SCIM("scim", "SCIM", "Access to System for Cross-domain Identity Management endpoint.", Collections.emptyList()),
+    CONSENT_ADMIN("consent_admin", "Consent_admin", "Access to End-User consents", Collections.emptyList());
 
     private final String key;
     private final String label;
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/approval/ApprovalService.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/approval/ApprovalService.java
index dcf84438b1..4d86b7c126 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/approval/ApprovalService.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/approval/ApprovalService.java
@@ -17,6 +17,7 @@ package io.gravitee.am.gateway.handler.oauth2.approval;
 
 import io.gravitee.am.gateway.handler.oauth2.request.AuthorizationRequest;
 import io.gravitee.am.model.Client;
+import io.gravitee.am.model.User;
 import io.reactivex.Single;
 
 /**
@@ -27,7 +28,7 @@ import io.reactivex.Single;
  */
 public interface ApprovalService {
 
-    Single<AuthorizationRequest> checkApproval(AuthorizationRequest authorizationRequest, Client client, String username);
+    Single<AuthorizationRequest> checkApproval(AuthorizationRequest authorizationRequest, Client client, User user);
 
-    Single<AuthorizationRequest> saveApproval(AuthorizationRequest authorizationRequest, Client client, String username);
+    Single<AuthorizationRequest> saveApproval(AuthorizationRequest authorizationRequest, Client client, User user);
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/approval/impl/ApprovalServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/approval/impl/ApprovalServiceImpl.java
index df129cc631..7766e31c45 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/approval/impl/ApprovalServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/approval/impl/ApprovalServiceImpl.java
@@ -22,6 +22,7 @@ import io.gravitee.am.gateway.handler.oauth2.scope.ScopeManager;
 import io.gravitee.am.gateway.handler.oauth2.utils.OAuth2Constants;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.User;
 import io.gravitee.am.model.oauth2.Scope;
 import io.gravitee.am.model.oauth2.ScopeApproval;
 import io.gravitee.am.repository.oauth2.api.ScopeApprovalRepository;
@@ -52,7 +53,7 @@ public class ApprovalServiceImpl implements ApprovalService {
     private int approvalExpirySeconds;
 
     @Override
-    public Single<AuthorizationRequest> checkApproval(AuthorizationRequest authorizationRequest, Client client, String username) {
+    public Single<AuthorizationRequest> checkApproval(AuthorizationRequest authorizationRequest, Client client, User user) {
         // check client auto approval option
         return checkAutoApproval(authorizationRequest, client)
                 .flatMap(authorizationRequest1 ->  {
@@ -60,12 +61,12 @@ public class ApprovalServiceImpl implements ApprovalService {
                         return Single.just(authorizationRequest1);
                     }
                     // check user approval
-                    return checkUserApproval(authorizationRequest, username);
+                    return checkUserApproval(authorizationRequest, user);
                 });
     }
 
     @Override
-    public Single<AuthorizationRequest> saveApproval(AuthorizationRequest authorizationRequest, Client client, String username) {
+    public Single<AuthorizationRequest> saveApproval(AuthorizationRequest authorizationRequest, Client client, User user) {
         // Get the unapproved requested scopes
         Set<String> requestedScopes = authorizationRequest.getDeniedScopes();
         Set<String> approvedScopes = new HashSet<>();
@@ -80,11 +81,11 @@ public class ApprovalServiceImpl implements ApprovalService {
             Date expiry = computeExpiry(client, requestedScope);
             if ("true".equals(value) || value.startsWith("approve")) {
                 approvedScopes.add(requestedScope);
-                approvals.add(new ScopeApproval(username, authorizationRequest.getClientId(),
+                approvals.add(new ScopeApproval(user.getId(), authorizationRequest.getClientId(),
                         requestedScope, ScopeApproval.ApprovalStatus.APPROVED, expiry, domain.getId()));
             }
             else {
-                approvals.add(new ScopeApproval(username, authorizationRequest.getClientId(),
+                approvals.add(new ScopeApproval(user.getId(), authorizationRequest.getClientId(),
                         requestedScope, ScopeApproval.ApprovalStatus.DENIED, expiry, domain.getId()));
             }
         }
@@ -106,10 +107,10 @@ public class ApprovalServiceImpl implements ApprovalService {
                 });
     }
 
-    private Single<AuthorizationRequest> checkUserApproval(AuthorizationRequest authorizationRequest, String username) {
+    private Single<AuthorizationRequest> checkUserApproval(AuthorizationRequest authorizationRequest, User user) {
         Set<String> requestedScopes = authorizationRequest.getScopes();
         Set<String> approvedScopes = new HashSet<>();
-        return scopeApprovalRepository.findByDomainAndUserAndClient(domain.getId(), username, authorizationRequest.getClientId())
+        return scopeApprovalRepository.findByDomainAndUserAndClient(domain.getId(), user.getId(), authorizationRequest.getClientId())
                 .flatMap(userApprovals -> {
                     // Look at the scopes and see if they have expired
                     if (userApprovals != null) {
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oidc/flow/AbstractFlow.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oidc/flow/AbstractFlow.java
index 1c8766f36a..b989fb752b 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oidc/flow/AbstractFlow.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oidc/flow/AbstractFlow.java
@@ -64,11 +64,7 @@ public abstract class AbstractFlow implements Flow {
     }
 
     private Single<AuthorizationRequest> obtainEndUserConsent(AuthorizationRequest authorizationRequest, Client client, User endUser) {
-        return approvalService.checkApproval(authorizationRequest, client, endUser.getUsername());
-    }
-
-    public AuthorizationRequestResolver getAuthorizationRequestResolver() {
-        return authorizationRequestResolver;
+        return approvalService.checkApproval(authorizationRequest, client, endUser);
     }
 
     public void setAuthorizationRequestResolver(AuthorizationRequestResolver authorizationRequestResolver) {
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/user/UserService.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/user/UserService.java
index b28520b886..bf768229df 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/user/UserService.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/user/UserService.java
@@ -18,10 +18,13 @@ package io.gravitee.am.gateway.handler.user;
 import io.gravitee.am.gateway.handler.user.model.UserToken;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.User;
+import io.gravitee.am.model.oauth2.ScopeApproval;
 import io.reactivex.Completable;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
 
+import java.util.Set;
+
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
  * @author GraviteeSource Team
@@ -37,4 +40,16 @@ public interface UserService {
     Completable resetPassword(User user);
 
     Completable forgotPassword(String email, Client client);
+
+    Single<Set<ScopeApproval>> consents(String userId);
+
+    Single<Set<ScopeApproval>> consents(String userId, String clientId);
+
+    Maybe<ScopeApproval> consent(String consentId);
+
+    Completable revokeConsent(String consentId);
+
+    Completable revokeConsents(String userId);
+
+    Completable revokeConsents(String userId, String clientId);
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/user/impl/UserServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/user/impl/UserServiceImpl.java
index dfb3f8d488..968df770ef 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/user/impl/UserServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/user/impl/UserServiceImpl.java
@@ -33,7 +33,10 @@ import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
 import io.gravitee.am.model.Template;
 import io.gravitee.am.model.User;
+import io.gravitee.am.model.oauth2.ScopeApproval;
 import io.gravitee.am.repository.management.api.UserRepository;
+import io.gravitee.am.service.ScopeApprovalService;
+import io.gravitee.am.service.exception.ScopeApprovalNotFoundException;
 import io.gravitee.am.service.exception.UserAlreadyExistsException;
 import io.gravitee.am.service.exception.UserNotFoundException;
 import io.gravitee.am.service.exception.UserProviderNotFoundException;
@@ -44,10 +47,7 @@ import io.reactivex.Single;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
@@ -90,6 +90,9 @@ public class UserServiceImpl implements UserService {
     @Autowired
     private ClientSyncService clientSyncService;
 
+    @Autowired
+    private ScopeApprovalService scopeApprovalService;
+
     @Override
     public Maybe<UserToken> verifyToken(String token) {
         return Maybe.fromCallable(() -> jwtParser.parse(token))
@@ -179,6 +182,37 @@ public class UserServiceImpl implements UserService {
 
     }
 
+    @Override
+    public Single<Set<ScopeApproval>> consents(String userId) {
+        return scopeApprovalService.findByDomainAndUser(domain.getId(), userId);
+    }
+
+    @Override
+    public Single<Set<ScopeApproval>> consents(String userId, String clientId) {
+        return scopeApprovalService.findByDomainAndUserAndClient(domain.getId(), userId, clientId);
+    }
+
+    @Override
+    public Maybe<ScopeApproval> consent(String consentId) {
+        return scopeApprovalService.findById(consentId)
+                .switchIfEmpty(Maybe.error(new ScopeApprovalNotFoundException(consentId)));
+    }
+
+    @Override
+    public Completable revokeConsent(String consentId) {
+        return scopeApprovalService.revoke(consentId);
+    }
+
+    @Override
+    public Completable revokeConsents(String userId) {
+        return scopeApprovalService.revoke(domain.getId(), userId);
+    }
+
+    @Override
+    public Completable revokeConsents(String userId, String clientId) {
+        return scopeApprovalService.revoke(domain.getId(), userId, clientId);
+    }
+
     private void completeForgotPassword(User user, Client client) {
         io.gravitee.am.model.Email email = emailManager.getEmail(getTemplateName(client), resetPasswordSubject, expireAfter);
         Email email1 = convert(user, client, email, "/resetPassword", "resetPasswordUrl");
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/VertxSecurityDomainHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/VertxSecurityDomainHandler.java
index 77b204ca21..e8292447e0 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/VertxSecurityDomainHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/VertxSecurityDomainHandler.java
@@ -27,6 +27,7 @@ import io.gravitee.am.gateway.handler.vertx.handler.root.RootRouter;
 import io.gravitee.am.gateway.handler.vertx.handler.scim.SCIMRouter;
 import io.gravitee.am.gateway.handler.vertx.handler.scim.handler.ErrorHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.session.RxSessionHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.users.UsersRouter;
 import io.gravitee.am.model.Domain;
 import io.vertx.core.Handler;
 import io.vertx.reactivex.core.Vertx;
@@ -72,6 +73,9 @@ public class VertxSecurityDomainHandler {
     @Autowired
     private SCIMRouter scimRouter;
 
+    @Autowired
+    private UsersRouter usersRouter;
+
     @Autowired
     private Environment environment;
 
@@ -111,6 +115,9 @@ public class VertxSecurityDomainHandler {
             router.mountSubRouter("/scim", scimRouter.route());
         }
 
+        // mount Users router
+        router.mountSubRouter("/users", usersRouter.route());
+
         return router;
     }
 
@@ -133,6 +140,7 @@ public class VertxSecurityDomainHandler {
         router.route("/forgotPassword").failureHandler(rootErrorFailureHandler);
         router.route("/oauth/authorize").failureHandler(authorizationEndpointFailureHandler);
         router.routeWithRegex("/scim/(.*)").failureHandler(new ErrorHandler());
+        router.routeWithRegex("/users/(.*)").failureHandler(new io.gravitee.am.gateway.handler.vertx.handler.users.handler.ErrorHandler());
         router.route().failureHandler(new ExceptionHandler());
     }
 
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/OAuth2Router.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/OAuth2Router.java
index 6b95ee04bc..e2020bc3f8 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/OAuth2Router.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/OAuth2Router.java
@@ -126,7 +126,7 @@ public class OAuth2Router {
         Handler<RoutingContext> authorizationRequestParseParametersHandler = new AuthorizationRequestParseParametersHandler(domain);
         Handler<RoutingContext> authorizeEndpoint = new AuthorizationEndpointHandler(flow, domain);
         Handler<RoutingContext> userApprovalRequestParseHandler = new UserApprovalRequestParseHandler(clientSyncService);
-        Handler<RoutingContext> userApprovalSubmissionEndpointHandler = new UserApprovalSubmissionEndpointHandler(approvalService);
+        Handler<RoutingContext> userApprovalSubmissionEndpointHandler = new UserApprovalSubmissionEndpointHandler(approvalService, domain);
         Handler<RoutingContext> userApprovalEndpoint = new UserApprovalEndpointHandler(scopeService, thymeleafTemplateEngine);
         // Token endpoint
         Handler<RoutingContext> tokenEndpoint = new TokenEndpointHandler(tokenGranter);
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AbstractAuthorizationEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AbstractAuthorizationEndpointHandler.java
index c2e6d35efc..a9359bebe5 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AbstractAuthorizationEndpointHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AbstractAuthorizationEndpointHandler.java
@@ -39,6 +39,10 @@ public abstract class AbstractAuthorizationEndpointHandler {
         }
 
         // the initial request failed for some reasons, we have the required request parameters to re-create the authorize request
+        return createAuthorizationRequest(routingContext);
+    }
+
+    protected AuthorizationRequest createAuthorizationRequest(RoutingContext routingContext) {
         return authorizationRequestFactory.create(routingContext.request());
     }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationEndpointHandler.java
index 17300776e5..8237c63985 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationEndpointHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationEndpointHandler.java
@@ -22,18 +22,23 @@ import io.gravitee.am.gateway.handler.oauth2.request.AuthorizationRequest;
 import io.gravitee.am.gateway.handler.oauth2.utils.OAuth2Constants;
 import io.gravitee.am.gateway.handler.oauth2.utils.OIDCParameters;
 import io.gravitee.am.gateway.handler.oidc.flow.Flow;
+import io.gravitee.am.gateway.handler.vertx.auth.handler.RedirectAuthHandler;
 import io.gravitee.am.gateway.handler.vertx.utils.UriBuilderRequest;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
 import io.gravitee.common.http.HttpHeaders;
 import io.vertx.core.Handler;
+import io.vertx.reactivex.core.http.HttpServerRequest;
 import io.vertx.reactivex.core.http.HttpServerResponse;
 import io.vertx.reactivex.ext.auth.User;
 import io.vertx.reactivex.ext.web.RoutingContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.net.URISyntaxException;
 import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * The authorization endpoint is used to interact with the resource owner and obtain an authorization grant.
@@ -59,7 +64,7 @@ public class AuthorizationEndpointHandler extends AbstractAuthorizationEndpointH
 
     @Override
     public void handle(RoutingContext context) {
-        AuthorizationRequest request = resolveInitialAuthorizeRequest(context);
+        AuthorizationRequest request = createAuthorizationRequest(context);
 
         // The authorization server authenticates the resource owner and obtains
         // an authorization decision (by asking the resource owner or by establishing approval via other means).
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalSubmissionEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalSubmissionEndpointHandler.java
index c9835c6639..68fa5b75bd 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalSubmissionEndpointHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalSubmissionEndpointHandler.java
@@ -19,8 +19,9 @@ import io.gravitee.am.gateway.handler.oauth2.approval.ApprovalService;
 import io.gravitee.am.gateway.handler.oauth2.exception.AccessDeniedException;
 import io.gravitee.am.gateway.handler.oauth2.request.AuthorizationRequest;
 import io.gravitee.am.gateway.handler.oauth2.utils.OAuth2Constants;
-import io.gravitee.am.gateway.handler.vertx.auth.handler.RedirectAuthHandler;
+import io.gravitee.am.gateway.handler.vertx.utils.UriBuilderRequest;
 import io.gravitee.am.model.Client;
+import io.gravitee.am.model.Domain;
 import io.gravitee.common.http.HttpHeaders;
 import io.vertx.core.Handler;
 import io.vertx.reactivex.core.MultiMap;
@@ -28,7 +29,6 @@ import io.vertx.reactivex.core.http.HttpServerRequest;
 import io.vertx.reactivex.core.http.HttpServerResponse;
 import io.vertx.reactivex.ext.auth.User;
 import io.vertx.reactivex.ext.web.RoutingContext;
-import io.vertx.reactivex.ext.web.Session;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -51,9 +51,11 @@ public class UserApprovalSubmissionEndpointHandler implements Handler<RoutingCon
     private static final Logger logger = LoggerFactory.getLogger(UserApprovalSubmissionEndpointHandler.class);
     private static final String CLIENT_CONTEXT_KEY = "client";
     private ApprovalService approvalService;
+    private Domain domain;
 
-    public UserApprovalSubmissionEndpointHandler(ApprovalService approvalService) {
+    public UserApprovalSubmissionEndpointHandler(ApprovalService approvalService, Domain domain) {
         this.approvalService = approvalService;
+        this.domain = domain;
     }
 
     @Override
@@ -84,7 +86,7 @@ public class UserApprovalSubmissionEndpointHandler implements Handler<RoutingCon
         authorizationRequest.setApprovalParameters(approvalParameters);
 
         // handle approval response
-        approvalService.saveApproval(authorizationRequest, client, endUser.getUsername())
+        approvalService.saveApproval(authorizationRequest, client, endUser)
                 .subscribe(authorizationRequest1 -> {
                     // user denied access
                     if (!authorizationRequest.isApproved()) {
@@ -93,13 +95,8 @@ public class UserApprovalSubmissionEndpointHandler implements Handler<RoutingCon
                     }
 
                     // user approved access, replay authorization request
-                    Session session = context.session();
-                    if (session != null && session.get(RedirectAuthHandler.DEFAULT_RETURN_URL_PARAM) != null) {
-                        final String redirectUrl = session.get(RedirectAuthHandler.DEFAULT_RETURN_URL_PARAM);
-                        doRedirect(context.response(), redirectUrl);
-                    } else {
-                        context.fail(503);
-                    }
+                    final String authorizationRequestUrl = UriBuilderRequest.resolveProxyRequest(context.request(),"/" + domain.getPath() + "/oauth/authorize", authorizationRequest.getRequestParameters().toSingleValueMap());
+                    doRedirect(context.response(), authorizationRequestUrl);
                 },
                 error -> {
                     logger.error("An error occurs while handling authorization approval request", error);
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/handler/BearerTokensParseHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/handler/BearerTokensParseHandler.java
index b881a88b1c..461e31a009 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/handler/BearerTokensParseHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/handler/BearerTokensParseHandler.java
@@ -90,6 +90,7 @@ public class BearerTokensParseHandler implements Handler<RoutingContext> {
                 Token token = handler.result();
                 if (token.getScope() == null || token.getScope().isEmpty() || !Arrays.asList(token.getScope().split("\\s+")).contains(SCIM_SCOPE)) {
                     context.fail(new InvalidTokenException("Invalid access token scopes. The access token should have at least 'scim' scope"));
+                    return;
                 }
 
                 context.next();
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/UsersRouter.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/UsersRouter.java
new file mode 100644
index 0000000000..9cfeaa6a80
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/UsersRouter.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.vertx.handler.users;
+
+import io.gravitee.am.gateway.handler.jwt.JwtService;
+import io.gravitee.am.gateway.handler.oauth2.client.ClientSyncService;
+import io.gravitee.am.gateway.handler.oauth2.token.TokenService;
+import io.gravitee.am.gateway.handler.user.UserService;
+import io.gravitee.am.gateway.handler.vertx.handler.users.endpoint.consents.UserConsentEndpointHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.users.endpoint.consents.UserConsentsEndpointHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.users.handler.AuthTokenParseHandler;
+import io.vertx.reactivex.core.Vertx;
+import io.vertx.reactivex.ext.web.Router;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class UsersRouter {
+
+    @Autowired
+    private Vertx vertx;
+
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private JwtService jwtService;
+
+    @Autowired
+    private ClientSyncService clientSyncService;
+
+    public Router route() {
+        // Create the Users router
+        final Router router = Router.router(vertx);
+
+        // Declare Users routes
+        final UserConsentsEndpointHandler userConsentsHandler = new UserConsentsEndpointHandler(userService);
+        final UserConsentEndpointHandler userConsentHandler = new UserConsentEndpointHandler(userService);
+
+
+        // User consent routes
+        router.routeWithRegex(".*consents.*")
+                .pathRegex("\\/(?<userId>[^\\/]+)\\/([^\\/]+)")
+                .handler(AuthTokenParseHandler.create(jwtService, tokenService, clientSyncService, "consent_admin"));
+        router.get("/:userId/consents")
+                .handler(userConsentsHandler::list);
+        router.delete("/:userId/consents")
+                .handler(userConsentsHandler::revoke);
+        router.get("/:userId/consents/:consentId")
+                .handler(userConsentHandler::get);
+        router.delete("/:userId/consents/:consentId")
+                .handler(userConsentHandler::revoke);
+
+        return router;
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentEndpointHandler.java
new file mode 100644
index 0000000000..bd5aa29055
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentEndpointHandler.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.vertx.handler.users.endpoint.consents;
+
+import io.gravitee.am.gateway.handler.user.UserService;
+import io.gravitee.common.http.HttpHeaders;
+import io.gravitee.common.http.MediaType;
+import io.vertx.core.json.Json;
+import io.vertx.reactivex.ext.web.RoutingContext;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class UserConsentEndpointHandler {
+
+    private UserService userService;
+
+    public UserConsentEndpointHandler(UserService userService) {
+        this.userService = userService;
+    }
+
+    /**
+     * Retrieve specific consent for a user
+     */
+    public void get(RoutingContext context) {
+        final String consentId = context.request().getParam("consentId");
+        userService.consent(consentId)
+                .subscribe(
+                        scopeApproval -> context.response()
+                                .putHeader(HttpHeaders.CACHE_CONTROL, "no-store")
+                                .putHeader(HttpHeaders.PRAGMA, "no-cache")
+                                .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+                                .end(Json.encodePrettily(scopeApproval)),
+                        error -> context.fail(error));
+    }
+
+    /**
+     * Revoke specific consent for a user
+     */
+    public void revoke(RoutingContext context) {
+        final String consentId = context.request().getParam("consentId");
+        userService.revokeConsent(consentId)
+                .subscribe(
+                        () -> context.response().setStatusCode(204).end(),
+                        error -> context.fail(error));
+    }
+
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentsEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentsEndpointHandler.java
new file mode 100644
index 0000000000..afba1134d8
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentsEndpointHandler.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.vertx.handler.users.endpoint.consents;
+
+import io.gravitee.am.gateway.handler.user.UserService;
+import io.gravitee.common.http.HttpHeaders;
+import io.gravitee.common.http.MediaType;
+import io.reactivex.Single;
+import io.vertx.core.json.Json;
+import io.vertx.reactivex.ext.web.RoutingContext;
+
+import java.util.Optional;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class UserConsentsEndpointHandler {
+
+    private UserService userService;
+
+    public UserConsentsEndpointHandler(UserService userService) {
+        this.userService = userService;
+    }
+
+    /**
+     * Retrieve consents for a user per application basis or for all applications
+     */
+    public void list(RoutingContext context) {
+        final String userId = context.request().getParam("userId");
+        final String clientId = context.request().getParam("clientId");
+
+        Single.just(Optional.ofNullable(clientId))
+                .flatMap(optClient -> {
+                    if (optClient.isPresent()) {
+                        return userService.consents(userId, optClient.get());
+                    }
+                    return userService.consents(userId);
+                })
+                .subscribe(
+                        scopeApprovals -> context.response()
+                                .putHeader(HttpHeaders.CACHE_CONTROL, "no-store")
+                                .putHeader(HttpHeaders.PRAGMA, "no-cache")
+                                .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+                                .end(Json.encodePrettily(scopeApprovals)),
+                        error -> context.fail(error));
+
+    }
+
+    /**
+     * Revoke consents for a user per application basis or for all applications
+     */
+    public void revoke(RoutingContext context) {
+        final String userId = context.request().getParam("userId");
+        final String clientId = context.request().getParam("clientId");
+
+        Single.just(Optional.ofNullable(clientId))
+                .flatMapCompletable(optClient -> {
+                    if (optClient.isPresent()) {
+                        return userService.revokeConsents(userId, optClient.get());
+                    }
+                    return userService.revokeConsents(userId);
+                })
+                .subscribe(
+                        () -> context.response().setStatusCode(204).end(),
+                        error -> context.fail(error));
+
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/handler/AuthTokenParseHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/handler/AuthTokenParseHandler.java
new file mode 100644
index 0000000000..1f9f3f7ddb
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/handler/AuthTokenParseHandler.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.vertx.handler.users.handler;
+
+import io.gravitee.am.gateway.handler.jwt.JwtService;
+import io.gravitee.am.gateway.handler.oauth2.client.ClientSyncService;
+import io.gravitee.am.gateway.handler.oauth2.exception.InvalidTokenException;
+import io.gravitee.am.gateway.handler.oauth2.token.Token;
+import io.gravitee.am.gateway.handler.oauth2.token.TokenService;
+import io.gravitee.common.http.HttpHeaders;
+import io.reactivex.Maybe;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.reactivex.core.http.HttpServerRequest;
+import io.vertx.reactivex.ext.web.RoutingContext;
+
+import java.util.Arrays;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class AuthTokenParseHandler implements Handler<RoutingContext> {
+
+    private static final String BEARER = "Bearer";
+    private String requiredScope;
+    private JwtService jwtService;
+    private TokenService tokenService;
+    private ClientSyncService clientService;
+
+    public AuthTokenParseHandler(JwtService jwtService, TokenService tokenService, ClientSyncService clientService) {
+        this.jwtService = jwtService;
+        this.tokenService = tokenService;
+        this.clientService = clientService;
+    }
+
+    public AuthTokenParseHandler(JwtService jwtService, TokenService tokenService, ClientSyncService clientService, String requiredScope) {
+        this(jwtService, tokenService, clientService);
+        this.requiredScope = requiredScope;
+    }
+
+    @Override
+    public void handle(RoutingContext context) {
+        parseAccessToken(context, parseHandler -> {
+            if (parseHandler.failed()) {
+                context.fail(parseHandler.cause());
+                return;
+            }
+
+            decodeAccessToken(parseHandler.result(), handler -> {
+                if (handler.failed()) {
+                    context.fail(handler.cause());
+                    return;
+                }
+
+                final String userId = context.request().getParam("userId");
+
+                Token token = handler.result();
+
+                // current user can show its account information
+                if (userId != null && userId.equals(token.getSubject())) {
+                    context.next();
+                    return;
+                }
+
+                // token must have required scope
+                if (token.getScope() == null || token.getScope().isEmpty() || !Arrays.asList(token.getScope().split("\\s+")).contains(requiredScope)) {
+                    context.fail(new InvalidTokenException("Invalid access token scopes. The access token should have at least '"+ requiredScope +"' scope"));
+                    return;
+                }
+
+                context.next();
+            });
+        });
+    }
+
+    public static AuthTokenParseHandler create(JwtService jwtService, TokenService tokenService, ClientSyncService clientService) {
+        return new AuthTokenParseHandler(jwtService, tokenService, clientService);
+    }
+
+    public static AuthTokenParseHandler create(JwtService jwtService, TokenService tokenService, ClientSyncService clientService, String requiredScope) {
+        return new AuthTokenParseHandler(jwtService, tokenService, clientService, requiredScope);
+    }
+
+    private void parseAccessToken(RoutingContext context, Handler<AsyncResult<String>> handler) {
+        final HttpServerRequest request = context.request();
+        final String authorization = request.headers().get(HttpHeaders.AUTHORIZATION);
+
+        if (authorization == null) {
+            handler.handle(Future.failedFuture(new InvalidTokenException("An access token is required")));
+            return;
+        }
+
+        int idx = authorization.indexOf(' ');
+        if (idx <= 0 || !BEARER.equalsIgnoreCase(authorization.substring(0, idx))) {
+            handler.handle(Future.failedFuture(new InvalidTokenException("The access token must be sent using the Authorization header field")));
+            return;
+        }
+
+        handler.handle(Future.succeededFuture(authorization.substring(idx + 1)));
+    }
+
+    private void decodeAccessToken(String accessToken, Handler<AsyncResult<Token>> handler) {
+        jwtService.decode(accessToken)
+                .flatMapMaybe(jwt -> clientService.findByClientId(jwt.getAud()).switchIfEmpty(Maybe.error(new InvalidTokenException("Invalid or unknown client for this token"))))
+                .flatMap(client -> tokenService.getAccessToken(accessToken, client)
+                        .map(accessToken1 -> {
+                            if (accessToken1.getExpiresIn() == 0) {
+                                throw new InvalidTokenException("The access token expired");
+                            }
+                            return accessToken1;
+                        })
+                )
+                .subscribe(
+                        accessToken1 -> handler.handle(Future.succeededFuture(accessToken1)),
+                        error -> handler.handle(Future.failedFuture(error)),
+                        () -> handler.handle(Future.failedFuture(new InvalidTokenException("The access token is invalid"))));
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/handler/ErrorHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/handler/ErrorHandler.java
new file mode 100644
index 0000000000..e03811cdd7
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/users/handler/ErrorHandler.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.vertx.handler.users.handler;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.gravitee.am.common.oauth2.exception.OAuth2Exception;
+import io.gravitee.am.service.exception.AbstractManagementException;
+import io.gravitee.common.http.HttpHeaders;
+import io.gravitee.common.http.HttpStatusCode;
+import io.gravitee.common.http.MediaType;
+import io.vertx.core.Handler;
+import io.vertx.core.json.Json;
+import io.vertx.reactivex.ext.web.RoutingContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class ErrorHandler implements Handler<RoutingContext> {
+
+    private static Logger logger = LoggerFactory.getLogger(io.gravitee.am.gateway.handler.vertx.handler.scim.handler.ErrorHandler.class);
+
+    @Override
+    public void handle(RoutingContext routingContext) {
+        if (routingContext.failed()) {
+            Throwable throwable = routingContext.failure();
+            // management exception (resource not found, server error, ...)
+            if (throwable instanceof AbstractManagementException) {
+                AbstractManagementException technicalManagementException = (AbstractManagementException) throwable;
+                handleException(routingContext, technicalManagementException.getHttpStatusCode(), technicalManagementException.getMessage());
+                // oauth2 exception (token invalid exception)
+            } else if (throwable instanceof OAuth2Exception) {
+                OAuth2Exception oAuth2Exception = (OAuth2Exception) throwable;
+                handleException(routingContext, oAuth2Exception.getHttpStatusCode(), oAuth2Exception.getMessage());
+            }
+            else {
+                logger.error(throwable.getMessage(), throwable);
+                if (routingContext.statusCode() != -1) {
+                    routingContext
+                            .response()
+                            .setStatusCode(routingContext.statusCode())
+                            .end();
+                } else {
+                    routingContext
+                            .response()
+                            .setStatusCode(HttpStatusCode.INTERNAL_SERVER_ERROR_500)
+                            .end();
+                }
+            }
+        }
+    }
+
+    private void handleException(RoutingContext routingContext, int httpStatusCode, String errorDetail) {
+            routingContext
+                .response()
+                .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+                .putHeader(HttpHeaders.CACHE_CONTROL, "no-store")
+                .putHeader(HttpHeaders.PRAGMA, "no-cache")
+                .setStatusCode(httpStatusCode)
+                .end(Json.encodePrettily(new Error(errorDetail, httpStatusCode)));
+    }
+
+    class Error {
+        private String message;
+
+        @JsonProperty("http_status")
+        private int httpCode;
+
+        public Error() {
+        }
+
+        public Error(String message, int status) {
+            this.message = message;
+            this.httpCode = status;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public void setMessage(String message) {
+            this.message = message;
+        }
+
+        public int getHttpCode() {
+            return httpCode;
+        }
+
+        public void setHttpCode(int httpCode) {
+            this.httpCode = httpCode;
+        }
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/spring/SecurityDomainRouterConfiguration.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/spring/SecurityDomainRouterConfiguration.java
index 6e850e03ca..80c06809e2 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/spring/SecurityDomainRouterConfiguration.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/spring/SecurityDomainRouterConfiguration.java
@@ -21,6 +21,7 @@ import io.gravitee.am.gateway.handler.vertx.handler.oauth2.OAuth2Router;
 import io.gravitee.am.gateway.handler.vertx.handler.oidc.OIDCRouter;
 import io.gravitee.am.gateway.handler.vertx.handler.root.RootRouter;
 import io.gravitee.am.gateway.handler.vertx.handler.scim.SCIMRouter;
+import io.gravitee.am.gateway.handler.vertx.handler.users.UsersRouter;
 import io.gravitee.am.gateway.handler.vertx.view.freemarker.FreeMarkerConfiguration;
 import io.gravitee.am.gateway.handler.vertx.view.thymeleaf.ThymeleafConfiguration;
 import io.gravitee.am.service.spring.ServiceConfiguration;
@@ -66,4 +67,9 @@ public class SecurityDomainRouterConfiguration {
     public SCIMRouter scimRouter() {
         return new SCIMRouter();
     }
+
+    @Bean
+    public UsersRouter usersRouter() {
+        return new UsersRouter();
+    }
 }
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/ClientEntity.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/ClientEntity.java
index ae59be394e..93721acad2 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/ClientEntity.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/ClientEntity.java
@@ -27,6 +27,12 @@ public class ClientEntity {
     private String clientId;
     private String clientName;
 
+    public ClientEntity(String id, String clientId, String clientName) {
+        this.id = id;
+        this.clientId = clientId;
+        this.clientName = clientName;
+    }
+
     public ClientEntity(Client client) {
         setId(client.getId());
         setClientId(client.getClientId());
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/ScopeApprovalEntity.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/ScopeApprovalEntity.java
new file mode 100644
index 0000000000..fd36db1cc9
--- /dev/null
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/ScopeApprovalEntity.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.management.handlers.management.api.model;
+
+import io.gravitee.am.model.oauth2.ScopeApproval;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class ScopeApprovalEntity extends ScopeApproval {
+
+    private ClientEntity clientEntity;
+
+    private ScopeEntity scopeEntity;
+
+    public ScopeApprovalEntity() {}
+
+    public ScopeApprovalEntity(ScopeApproval scopeApproval) {
+        setId(scopeApproval.getId());
+        setUserId(scopeApproval.getUserId());
+        setClientId(scopeApproval.getClientId());
+        setDomain(scopeApproval.getDomain());
+        setScope(scopeApproval.getScope());
+        setStatus(scopeApproval.getStatus());
+        setExpiresAt(scopeApproval.getExpiresAt());
+        setCreatedAt(scopeApproval.getCreatedAt());
+        setUpdatedAt(scopeApproval.getUpdatedAt());
+    }
+
+    public ClientEntity getClientEntity() {
+        return clientEntity;
+    }
+
+    public void setClientEntity(ClientEntity clientEntity) {
+        this.clientEntity = clientEntity;
+    }
+
+    public ScopeEntity getScopeEntity() {
+        return scopeEntity;
+    }
+
+    public void setScopeEntity(ScopeEntity scopeEntity) {
+        this.scopeEntity = scopeEntity;
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/ScopeEntity.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/ScopeEntity.java
new file mode 100644
index 0000000000..3c71079a08
--- /dev/null
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/ScopeEntity.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.management.handlers.management.api.model;
+
+import io.gravitee.am.model.oauth2.Scope;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class ScopeEntity {
+
+    private String id;
+    private String key;
+    private String name;
+    private String description;
+
+    public ScopeEntity() {
+    }
+
+    public ScopeEntity(String id, String key, String name, String description) {
+        this.id = id;
+        this.key = key;
+        this.name = name;
+        this.description = description;
+    }
+
+    public ScopeEntity(Scope scope) {
+        setId(scope.getId());
+        setKey(scope.getKey());
+        setName(scope.getName());
+        setDescription(scope.getDescription());
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentResource.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentResource.java
new file mode 100644
index 0000000000..e0d852c0b3
--- /dev/null
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentResource.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.management.handlers.management.api.resources;
+
+import io.gravitee.am.management.handlers.management.api.model.ClientEntity;
+import io.gravitee.am.management.handlers.management.api.model.ScopeApprovalEntity;
+import io.gravitee.am.service.ClientService;
+import io.gravitee.am.service.DomainService;
+import io.gravitee.am.service.ScopeApprovalService;
+import io.gravitee.am.service.exception.DomainNotFoundException;
+import io.gravitee.am.service.exception.ScopeApprovalNotFoundException;
+import io.gravitee.common.http.MediaType;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.ws.rs.*;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Response;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class UserConsentResource {
+
+    @Autowired
+    private DomainService domainService;
+
+    @Autowired
+    private ScopeApprovalService scopeApprovalService;
+
+    @Autowired
+    private ClientService clientService;
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(value = "Get a user consent")
+    @ApiResponses({
+            @ApiResponse(code = 200, message = "User consent successfully fetched", response = ScopeApprovalEntity.class),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void list(@PathParam("domain") String domain,
+                     @PathParam("user") String user,
+                     @PathParam("consent") String consent,
+                     @Suspended final AsyncResponse response) {
+
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMap(__ -> scopeApprovalService.findById(consent))
+                .switchIfEmpty(Maybe.error(new ScopeApprovalNotFoundException(consent)))
+                .flatMapSingle(scopeApproval -> getClient(scopeApproval.getDomain(), scopeApproval.getClientId())
+                        .map(clientEntity -> {
+                            ScopeApprovalEntity scopeApprovalEntity = new ScopeApprovalEntity(scopeApproval);
+                            scopeApprovalEntity.setClientEntity(clientEntity);
+                            return scopeApprovalEntity;
+                        }))
+                .map(scopeApproval ->  Response.ok(scopeApproval).build())
+                .subscribe(
+                        result -> response.resume(result),
+                        error -> response.resume(error));
+    }
+
+    @DELETE
+    @ApiOperation(value = "Revoke user consent")
+    @ApiResponses({
+            @ApiResponse(code = 204, message = "User consent successfully revoked"),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void delete(@PathParam("domain") String domain,
+                       @PathParam("user") String user,
+                       @PathParam("consent") String consent,
+                       @Suspended final AsyncResponse response) {
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMapCompletable(__ -> scopeApprovalService.revoke(consent))
+                .subscribe(
+                        () -> response.resume(Response.noContent().build()),
+                        error -> response.resume(error));
+    }
+
+
+    private Single<ClientEntity> getClient(String domain, String clientId) {
+        return clientService.findByDomainAndClientId(domain, clientId)
+                .map(client -> new ClientEntity(client))
+                .defaultIfEmpty(new ClientEntity("unknown-id", clientId, "unknown-client-name"))
+                .toSingle();
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentsResource.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentsResource.java
new file mode 100644
index 0000000000..f9c894b8ff
--- /dev/null
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentsResource.java
@@ -0,0 +1,141 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.management.handlers.management.api.resources;
+
+import io.gravitee.am.management.handlers.management.api.model.ClientEntity;
+import io.gravitee.am.management.handlers.management.api.model.ScopeApprovalEntity;
+import io.gravitee.am.management.handlers.management.api.model.ScopeEntity;
+import io.gravitee.am.service.ClientService;
+import io.gravitee.am.service.DomainService;
+import io.gravitee.am.service.ScopeApprovalService;
+import io.gravitee.am.service.ScopeService;
+import io.gravitee.am.service.exception.DomainNotFoundException;
+import io.gravitee.common.http.MediaType;
+import io.reactivex.Maybe;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.ws.rs.*;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.ResourceContext;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class UserConsentsResource {
+
+    @Context
+    private ResourceContext resourceContext;
+
+    @Autowired
+    private DomainService domainService;
+
+    @Autowired
+    private ScopeApprovalService scopeApprovalService;
+
+    @Autowired
+    private ClientService clientService;
+
+    @Autowired
+    private ScopeService scopeService;
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(value = "Get a user consents")
+    @ApiResponses({
+            @ApiResponse(code = 200, message = "User consents successfully fetched", response = ScopeApprovalEntity.class),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void list(@PathParam("domain") String domain,
+                     @PathParam("user") String user,
+                     @QueryParam("clientId") String clientId,
+                     @Suspended final AsyncResponse response) {
+
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMapSingle(__ -> {
+                    if (clientId == null || clientId.isEmpty()) {
+                        return scopeApprovalService.findByDomainAndUser(domain, user);
+                    }
+                    return scopeApprovalService.findByDomainAndUserAndClient(domain, user, clientId);
+                })
+                .flatMapObservable(scopeApprovals -> Observable.fromIterable(scopeApprovals))
+                .flatMapSingle(scopeApproval ->
+                        getClient(scopeApproval.getDomain(), scopeApproval.getClientId())
+                                .zipWith(getScope(scopeApproval.getDomain(), scopeApproval.getScope()), ((clientEntity, scopeEntity) -> {
+                                    ScopeApprovalEntity scopeApprovalEntity = new ScopeApprovalEntity(scopeApproval);
+                                    scopeApprovalEntity.setClientEntity(clientEntity);
+                                    scopeApprovalEntity.setScopeEntity(scopeEntity);
+                                    return scopeApprovalEntity;
+                                })))
+                .toList()
+                .map(scopeApprovals ->  Response.ok(scopeApprovals).build())
+                .subscribe(
+                        result -> response.resume(result),
+                        error -> response.resume(error));
+    }
+
+    @DELETE
+    @ApiOperation(value = "Revoke user consents")
+    @ApiResponses({
+            @ApiResponse(code = 204, message = "User consents successfully revoked"),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void delete(@PathParam("domain") String domain,
+                       @PathParam("user") String user,
+                       @QueryParam("clientId") String clientId,
+                       @Suspended final AsyncResponse response) {
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMapCompletable(__ -> {
+                    if (clientId == null || clientId.isEmpty()) {
+                        return scopeApprovalService.revoke(domain, user);
+                    }
+                    return scopeApprovalService.revoke(domain, user, clientId);
+                })
+                .subscribe(
+                        () -> response.resume(Response.noContent().build()),
+                        error -> response.resume(error));
+    }
+
+    @Path("{consent}")
+    public UserConsentResource getUserConsentResource() {
+        return resourceContext.getResource(UserConsentResource.class);
+    }
+
+    private Single<ClientEntity> getClient(String domain, String clientId) {
+        return clientService.findByDomainAndClientId(domain, clientId)
+                .map(client -> new ClientEntity(client))
+                .defaultIfEmpty(new ClientEntity("unknown-id", clientId, "unknown-client-name"))
+                .toSingle()
+                .cache();
+    }
+
+    private Single<ScopeEntity> getScope(String domain, String scopeKey) {
+        return scopeService.findByDomainAndKey(domain, scopeKey)
+                .map(scope -> new ScopeEntity(scope))
+                .defaultIfEmpty(new ScopeEntity("unknown-id", scopeKey, "unknown-scope-name", "unknown-scope-description"))
+                .toSingle()
+                .cache();
+    }
+
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java
index 69c726ae55..8b357830de 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java
@@ -180,6 +180,11 @@ public class UserResource {
 
     }
 
+    @Path("{consents}")
+    public UserConsentsResource getUserConsentsResource() {
+        return resourceContext.getResource(UserConsentsResource.class);
+    }
+
     private Maybe<UserEntity> enhanceIdentityProvider(UserEntity userEntity) {
         if (userEntity.getSource() != null) {
             return identityProviderService.findById(userEntity.getSource())
diff --git a/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/ScopeApprovalRepositoryProxy.java b/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/ScopeApprovalRepositoryProxy.java
index aa4851d93d..4b873f1031 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/ScopeApprovalRepositoryProxy.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/ScopeApprovalRepositoryProxy.java
@@ -26,13 +26,22 @@ import java.util.Set;
 
 /**
  * @author David BRASSELY (david.brassely at graviteesource.com)
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
  * @author GraviteeSource Team
  */
 @Component
 public class ScopeApprovalRepositoryProxy extends AbstractProxy<ScopeApprovalRepository> implements ScopeApprovalRepository {
 
-    public Maybe<ScopeApproval> findById(String s) {
-        return target.findById(s);
+    public Single<Set<ScopeApproval>> findByDomainAndUserAndClient(String domain, String userId, String clientId) {
+        return target.findByDomainAndUserAndClient(domain, userId, clientId);
+    }
+
+    public Single<Set<ScopeApproval>> findByDomainAndUser(String domain, String user) {
+        return target.findByDomainAndUser(domain, user);
+    }
+
+    public Maybe<ScopeApproval> findById(String id) {
+        return target.findById(id);
     }
 
     public Single<ScopeApproval> create(ScopeApproval item) {
@@ -43,19 +52,23 @@ public class ScopeApprovalRepositoryProxy extends AbstractProxy<ScopeApprovalRep
         return target.update(item);
     }
 
-    public Completable delete(String s) {
-        return target.delete(s);
+    public Single<ScopeApproval> upsert(ScopeApproval scopeApproval) {
+        return target.upsert(scopeApproval);
+    }
+
+    public Completable delete(String id) {
+        return target.delete(id);
     }
 
-    public Single<Set<ScopeApproval>> findByDomainAndUserAndClient(String domain, String userId, String clientId) {
-        return target.findByDomainAndUserAndClient(domain, userId, clientId);
+    public Completable deleteByDomainAndScopeKey(String domain, String scope) {
+        return target.deleteByDomainAndScopeKey(domain, scope);
     }
 
-    public Single<ScopeApproval> upsert(ScopeApproval scopeApproval) {
-        return target.upsert(scopeApproval);
+    public Completable deleteByDomainAndUserAndClient(String domain, String user, String client) {
+        return target.deleteByDomainAndUserAndClient(domain, user, client);
     }
 
-    public Completable delete(String domain, String scope) {
-        return target.delete(domain, scope);
+    public Completable deleteByDomainAndUser(String domain, String user) {
+        return target.deleteByDomainAndUser(domain, user);
     }
 }
diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/oauth2/ScopeApproval.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/oauth2/ScopeApproval.java
index e103596f9a..5d87dba029 100644
--- a/gravitee-am-model/src/main/java/io/gravitee/am/model/oauth2/ScopeApproval.java
+++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/oauth2/ScopeApproval.java
@@ -19,41 +19,51 @@ import java.util.Date;
 
 /**
  * @author David BRASSELY (david.brassely at graviteesource.com)
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
  * @author GraviteeSource Team
  */
 public class ScopeApproval {
 
+    public enum ApprovalStatus {
+        APPROVED,
+        DENIED
+    }
+
+    private String id;
+
     private String userId;
 
     private String clientId;
 
-    private String scope;
+    private String domain;
 
-    public enum ApprovalStatus {
-        APPROVED,
-        DENIED;
-    }
+    private String scope;
 
     private ApprovalStatus status;
 
     private Date expiresAt;
 
-    private Date updatedAt;
+    private Date createdAt;
 
-    /**
-     * Security domain associated to the scope
-     */
-    private String domain;
+    private Date updatedAt;
 
     public ScopeApproval() {}
 
     public ScopeApproval(String userId, String clientId, String scope, ApprovalStatus status, Date expiresAt, String domain) {
         this.userId = userId;
         this.clientId = clientId;
+        this.domain = domain;
         this.scope = scope;
         this.status = status;
         this.expiresAt = expiresAt;
-        this.domain = domain;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
     }
 
     public String getUserId() {
@@ -72,6 +82,14 @@ public class ScopeApproval {
         this.clientId = clientId;
     }
 
+    public String getDomain() {
+        return domain;
+    }
+
+    public void setDomain(String domain) {
+        this.domain = domain;
+    }
+
     public String getScope() {
         return scope;
     }
@@ -96,20 +114,20 @@ public class ScopeApproval {
         this.expiresAt = expiresAt;
     }
 
-    public Date getUpdatedAt() {
-        return updatedAt;
+    public Date getCreatedAt() {
+        return createdAt;
     }
 
-    public void setUpdatedAt(Date updatedAt) {
-        this.updatedAt = updatedAt;
+    public void setCreatedAt(Date createdAt) {
+        this.createdAt = createdAt;
     }
 
-    public String getDomain() {
-        return domain;
+    public Date getUpdatedAt() {
+        return updatedAt;
     }
 
-    public void setDomain(String domain) {
-        this.domain = domain;
+    public void setUpdatedAt(Date updatedAt) {
+        this.updatedAt = updatedAt;
     }
 
     @Override
diff --git a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/api/ScopeApprovalRepository.java b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/api/ScopeApprovalRepository.java
index 65f4968c44..a8a19f193e 100644
--- a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/api/ScopeApprovalRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/api/ScopeApprovalRepository.java
@@ -24,13 +24,21 @@ import java.util.Set;
 
 /**
  * @author David BRASSELY (david.brassely at graviteesource.com)
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
  * @author GraviteeSource Team
  */
 public interface ScopeApprovalRepository extends CrudRepository<ScopeApproval, String> {
 
     Single<Set<ScopeApproval>> findByDomainAndUserAndClient(String domain, String userId, String clientId);
 
+    Single<Set<ScopeApproval>> findByDomainAndUser(String domain, String user);
+
     Single<ScopeApproval> upsert(ScopeApproval scopeApproval);
 
-    Completable delete(String domain, String scope);
+    Completable deleteByDomainAndScopeKey(String domain, String scope);
+
+    Completable deleteByDomainAndUserAndClient(String domain, String user, String client);
+
+    Completable deleteByDomainAndUser(String domain, String user);
+
 }
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoScopeApprovalRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoScopeApprovalRepository.java
index 593195373a..2f034c8f45 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoScopeApprovalRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoScopeApprovalRepository.java
@@ -61,13 +61,24 @@ public class MongoScopeApprovalRepository extends AbstractOAuth2MongoRepository
     public void init() {
         scopeApprovalsCollection = mongoOperations.getCollection("scope_approvals", ScopeApprovalMongo.class);
         scopeApprovalsCollection.createIndex(new Document(FIELD_EXPIRES_AT, 1),  new IndexOptions().expireAfter(0l, TimeUnit.SECONDS)).subscribe(new IndexSubscriber());
+        scopeApprovalsCollection.createIndex(new Document(FIELD_DOMAIN, 1).append(FIELD_USER_ID, 1)).subscribe(new IndexSubscriber());
         scopeApprovalsCollection.createIndex(new Document(FIELD_DOMAIN, 1).append(FIELD_CLIENT_ID, 1).append(FIELD_USER_ID, 1)).subscribe(new IndexSubscriber());
         scopeApprovalsCollection.createIndex(new Document(FIELD_DOMAIN, 1).append(FIELD_CLIENT_ID, 1).append(FIELD_USER_ID, 1).append(FIELD_SCOPE, 1)).subscribe(new IndexSubscriber());
     }
 
     @Override
-    public Maybe<ScopeApproval> findById(String s) {
-        throw new IllegalStateException();
+    public Single<Set<ScopeApproval>> findByDomainAndUserAndClient(String domain, String userId, String clientId) {
+        return Observable.fromPublisher(scopeApprovalsCollection.find(and(eq(FIELD_DOMAIN, domain), eq(FIELD_CLIENT_ID, clientId), eq(FIELD_USER_ID, userId)))).map(this::convert).collect(HashSet::new, Set::add);
+    }
+
+    @Override
+    public Single<Set<ScopeApproval>> findByDomainAndUser(String domain, String user) {
+        return Observable.fromPublisher(scopeApprovalsCollection.find(and(eq(FIELD_DOMAIN, domain), eq(FIELD_USER_ID, user)))).map(this::convert).collect(HashSet::new, Set::add);
+    }
+
+    @Override
+    public Maybe<ScopeApproval> findById(String id) {
+        return Observable.fromPublisher(scopeApprovalsCollection.find(eq(FIELD_ID, id)).first()).firstElement().map(this::convert);
     }
 
     @Override
@@ -100,6 +111,8 @@ public class MongoScopeApprovalRepository extends AbstractOAuth2MongoRepository
                 .isEmpty()
                 .flatMap(isEmpty -> {
                     if (isEmpty) {
+                        scopeApproval.setCreatedAt(new Date());
+                        scopeApproval.setUpdatedAt(scopeApproval.getCreatedAt());
                         return create(scopeApproval);
                     } else {
                         scopeApproval.setUpdatedAt(new Date());
@@ -109,7 +122,7 @@ public class MongoScopeApprovalRepository extends AbstractOAuth2MongoRepository
     }
 
     @Override
-    public Completable delete(String domain, String scope) {
+    public Completable deleteByDomainAndScopeKey(String domain, String scope) {
         return Completable.fromPublisher(scopeApprovalsCollection.deleteMany(
                 and(eq(FIELD_DOMAIN, domain), eq(FIELD_SCOPE, scope))));
     }
@@ -120,8 +133,15 @@ public class MongoScopeApprovalRepository extends AbstractOAuth2MongoRepository
     }
 
     @Override
-    public Single<Set<ScopeApproval>> findByDomainAndUserAndClient(String domain, String userId, String clientId) {
-        return Observable.fromPublisher(scopeApprovalsCollection.find(and(eq(FIELD_DOMAIN, domain), eq(FIELD_CLIENT_ID, clientId), eq(FIELD_USER_ID, userId)))).map(this::convert).collect(HashSet::new, Set::add);
+    public Completable deleteByDomainAndUserAndClient(String domain, String user, String client) {
+        return Completable.fromPublisher(scopeApprovalsCollection.deleteMany(
+                and(eq(FIELD_DOMAIN, domain), eq(FIELD_USER_ID, user), eq(FIELD_CLIENT_ID, client))));
+    }
+
+    @Override
+    public Completable deleteByDomainAndUser(String domain, String user) {
+        return Completable.fromPublisher(scopeApprovalsCollection.deleteMany(
+                and(eq(FIELD_DOMAIN, domain), eq(FIELD_USER_ID, user))));
     }
 
     private Single<ScopeApproval> _findById(String id) {
@@ -134,12 +154,14 @@ public class MongoScopeApprovalRepository extends AbstractOAuth2MongoRepository
         }
 
         ScopeApproval scopeApproval = new ScopeApproval();
+        scopeApproval.setId(scopeApprovalMongo.getId());
         scopeApproval.setClientId(scopeApprovalMongo.getClientId());
         scopeApproval.setUserId(scopeApprovalMongo.getUserId());
         scopeApproval.setScope(scopeApprovalMongo.getScope());
         scopeApproval.setExpiresAt(scopeApprovalMongo.getExpiresAt());
         scopeApproval.setStatus(ScopeApproval.ApprovalStatus.valueOf(scopeApprovalMongo.getStatus().toUpperCase()));
         scopeApproval.setDomain(scopeApprovalMongo.getDomain());
+        scopeApproval.setCreatedAt(scopeApprovalMongo.getCreatedAt());
         scopeApproval.setUpdatedAt(scopeApprovalMongo.getUpdatedAt());
 
         return scopeApproval;
@@ -151,12 +173,14 @@ public class MongoScopeApprovalRepository extends AbstractOAuth2MongoRepository
         }
 
         ScopeApprovalMongo scopeApprovalMongo = new ScopeApprovalMongo();
+        scopeApprovalMongo.setId(scopeApproval.getId());
         scopeApprovalMongo.setClientId(scopeApproval.getClientId());
         scopeApprovalMongo.setUserId(scopeApproval.getUserId());
         scopeApprovalMongo.setScope(scopeApproval.getScope());
         scopeApprovalMongo.setExpiresAt(scopeApproval.getExpiresAt());
         scopeApprovalMongo.setStatus(scopeApproval.getStatus().name().toUpperCase());
         scopeApprovalMongo.setDomain(scopeApproval.getDomain());
+        scopeApprovalMongo.setCreatedAt(scopeApproval.getCreatedAt());
         scopeApprovalMongo.setUpdatedAt(scopeApproval.getUpdatedAt());
 
         return scopeApprovalMongo;
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeApprovalService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeApprovalService.java
new file mode 100644
index 0000000000..2f49761862
--- /dev/null
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeApprovalService.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.service;
+
+import io.gravitee.am.model.oauth2.ScopeApproval;
+import io.reactivex.Completable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+
+import java.util.Set;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface ScopeApprovalService {
+
+    Maybe<ScopeApproval> findById(String id);
+
+    Single<Set<ScopeApproval>> findByDomainAndUser(String domain, String user);
+
+    Single<Set<ScopeApproval>> findByDomainAndUserAndClient(String domain, String user, String client);
+
+    Completable revoke(String consentId);
+
+    Completable revoke(String domain, String user);
+
+    Completable revoke(String domain, String user, String clientId);
+}
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeService.java
index 0349010038..2c52abf859 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeService.java
@@ -21,6 +21,7 @@ import io.gravitee.am.service.model.NewSystemScope;
 import io.gravitee.am.service.model.UpdateScope;
 import io.gravitee.am.service.model.UpdateSystemScope;
 import io.reactivex.Completable;
+import io.reactivex.Flowable;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
 
@@ -43,6 +44,8 @@ public interface ScopeService {
 
     Single<Set<Scope>> findByDomain(String domain);
 
+    Maybe<Scope> findByDomainAndKey(String domain, String scopeKey);
+
     Single<Scope> update(String domain, String id, UpdateScope updateScope);
 
     Single<Scope> update(String domain, String id, UpdateSystemScope updateScope);
@@ -54,4 +57,5 @@ public interface ScopeService {
      * @param scopes Array of scope to validate.
      */
     Single<Boolean> validateScope(String domain, List<String> scopes);
+
 }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/ScopeApprovalNotFoundException.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/ScopeApprovalNotFoundException.java
new file mode 100644
index 0000000000..ba8bc99a9c
--- /dev/null
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/ScopeApprovalNotFoundException.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.service.exception;
+
+/**
+ * @author David BRASSELY (david.brassely at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class ScopeApprovalNotFoundException extends AbstractNotFoundException {
+
+    private final String scope;
+
+    public ScopeApprovalNotFoundException(String scope) {
+        this.scope = scope;
+    }
+
+    @Override
+    public String getMessage() {
+        return "Scope approval [" + scope + "] can not be found.";
+    }
+
+}
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ScopeApprovalServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ScopeApprovalServiceImpl.java
new file mode 100644
index 0000000000..7bce83f4e6
--- /dev/null
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ScopeApprovalServiceImpl.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.service.impl;
+
+import io.gravitee.am.model.oauth2.ScopeApproval;
+import io.gravitee.am.repository.oauth2.api.ScopeApprovalRepository;
+import io.gravitee.am.service.ScopeApprovalService;
+import io.gravitee.am.service.exception.TechnicalManagementException;
+import io.reactivex.Completable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@Component
+public class ScopeApprovalServiceImpl implements ScopeApprovalService {
+
+    public static final Logger LOGGER = LoggerFactory.getLogger(ScopeApprovalServiceImpl.class);
+
+    @Autowired
+    private ScopeApprovalRepository scopeApprovalRepository;
+
+    @Override
+    public Maybe<ScopeApproval> findById(String id) {
+        LOGGER.debug("Find scope approval by id: {}", id);
+        return scopeApprovalRepository.findById(id)
+                .onErrorResumeNext(ex -> {
+                    LOGGER.error("An error occurs while trying to find a scope approval by id: {}", id);
+                    return Maybe.error(new TechnicalManagementException(
+                            String.format("An error occurs while trying to find a scope approval by id %s", id), ex));
+                });
+    }
+
+    @Override
+    public Single<Set<ScopeApproval>> findByDomainAndUser(String domain, String user) {
+        LOGGER.debug("Find scope approvals by domain: {} and user: {}", domain, user);
+        return scopeApprovalRepository.findByDomainAndUser(domain, user)
+                .onErrorResumeNext(ex -> {
+                    LOGGER.error("An error occurs while trying to find a scope approval for domain: {} and user: {}", domain, user);
+                    return Single.error(new TechnicalManagementException(
+                            String.format("An error occurs while trying to find a scope approval for domain: %s and user: %s", domain, user), ex));
+                });
+    }
+
+    @Override
+    public Single<Set<ScopeApproval>> findByDomainAndUserAndClient(String domain, String user, String client) {
+        LOGGER.debug("Find scope approvals by domain: {} and user: {} and client: {}", domain, user);
+        return scopeApprovalRepository.findByDomainAndUserAndClient(domain, user, client)
+                .onErrorResumeNext(ex -> {
+                    LOGGER.error("An error occurs while trying to find a scope approval for domain: {}, user: {} and client: {}", domain, user, client);
+                    return Single.error(new TechnicalManagementException(
+                            String.format("An error occurs while trying to find a scope approval for domain: %s, user: %s and client: %s", domain, user, client), ex));
+                });
+    }
+
+    @Override
+    public Completable revoke(String consentId) {
+        LOGGER.debug("Revoke approval for consent: {}", consentId);
+        return scopeApprovalRepository.delete(consentId)
+                .onErrorResumeNext(ex -> {
+                    LOGGER.error("An error occurs while trying to revoke approval for scope: {}", consentId);
+                    return Completable.error(new TechnicalManagementException(
+                            String.format("An error occurs while trying to revoke approval for scope: %s", consentId), ex));
+                });
+    }
+
+    @Override
+    public Completable revoke(String domain, String user) {
+        LOGGER.debug("Revoke approvals for domain: {} and user: {}", domain, user);
+        return scopeApprovalRepository.deleteByDomainAndUser(domain, user)
+                .onErrorResumeNext(ex -> {
+                    LOGGER.error("An error occurs while trying to revoke scope approvals for domain: {} and user : {}", domain, user);
+                    return Completable.error(new TechnicalManagementException(
+                            String.format("An error occurs while trying to revoke scope approvals for domain: %s and user: %s", domain, user), ex));
+                });
+    }
+
+    @Override
+    public Completable revoke(String domain, String user, String clientId) {
+        LOGGER.debug("Revoke approvals for domain: {}, user: {} and client: {}", domain, user, clientId);
+        return scopeApprovalRepository.deleteByDomainAndUserAndClient(domain, user, clientId)
+                .onErrorResumeNext(ex -> {
+                    LOGGER.error("An error occurs while trying to revoke scope approvals for domain: {}, user: {} and client: {}", domain, user, clientId);
+                    return Completable.error(new TechnicalManagementException(
+                            String.format("An error occurs while trying to revoke scope approvals for domain: %s, user: %s and client: %s", domain, user, clientId), ex));
+                });
+    }
+}
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ScopeServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ScopeServiceImpl.java
index 354c66f137..29a05f778b 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ScopeServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ScopeServiceImpl.java
@@ -256,7 +256,7 @@ public class ScopeServiceImpl implements ScopeService {
                                                     return clientService.patch(scope.getDomain(), client.getId(), patchClient);
                                                 }).toList()).toCompletable()
                                 // 3_ Remove scopes from scope_approvals
-                                .andThen(scopeApprovalRepository.delete(scope.getDomain(), scope.getKey()))
+                                .andThen(scopeApprovalRepository.deleteByDomainAndScopeKey(scope.getDomain(), scope.getKey()))
                                 // 4_ Delete scope
                                 .andThen(scopeRepository.delete(scopeId))
                                 // 5_ reload domain
@@ -278,7 +278,7 @@ public class ScopeServiceImpl implements ScopeService {
 
     @Override
     public Single<Set<Scope>> findByDomain(String domain) {
-        LOGGER.debug("Find scopes by domain", domain);
+        LOGGER.debug("Find scopes by domain: {}", domain);
         return scopeRepository.findByDomain(domain)
                 .onErrorResumeNext(ex -> {
                     LOGGER.error("An error occurs while trying to find scopes by domain: {}", domain, ex);
@@ -287,6 +287,17 @@ public class ScopeServiceImpl implements ScopeService {
                 });
     }
 
+    @Override
+    public Maybe<Scope> findByDomainAndKey(String domain, String scopeKey) {
+        LOGGER.debug("Find scopes by domain: {} and scope key: {}", domain, scopeKey);
+        return scopeRepository.findByDomainAndKey(domain, scopeKey)
+                .onErrorResumeNext(ex -> {
+                    LOGGER.error("An error occurs while trying to find scopes by domain: {} and scope key: {}", domain, scopeKey, ex);
+                    return Maybe.error(new TechnicalManagementException(
+                            String.format("An error occurs while trying to find scopes by domain: %s and scope key: %s", domain, scopeKey), ex));
+                });
+    }
+
     /**
      * Throw InvalidClientMetadataException if null or empty, or contains unknown scope.
      * @param scopes Array of scope to validate.
diff --git a/gravitee-am-ui/src/app/app-routing.module.ts b/gravitee-am-ui/src/app/app-routing.module.ts
index b426e43a37..0f49d3065e 100644
--- a/gravitee-am-ui/src/app/app-routing.module.ts
+++ b/gravitee-am-ui/src/app/app-routing.module.ts
@@ -77,6 +77,8 @@ import {UsersResolver} from "./resolvers/users.resolver";
 import {UserComponent} from "./domain/settings/users/user/user.component";
 import {UserResolver} from "./resolvers/user.resolver";
 import {UserCreationComponent} from "./domain/settings/users/creation/user-creation.component";
+import {UserProfileComponent} from "./domain/settings/users/user/profile/profile.component";
+import {UserApplicationsComponent} from "./domain/settings/users/user/applications/applications.component";
 import {ExtensionGrantCreationComponent} from "./domain/settings/extension-grants/creation/extension-grant-creation.component";
 import {ExtensionGrantComponent} from "./domain/settings/extension-grants/extension-grant/extension-grant.component";
 import {ExtensionGrantsResolver} from "./resolvers/extension-grants.resolver";
@@ -93,6 +95,8 @@ import {GroupSettingsComponent} from "./domain/settings/groups/group/settings/se
 import {GroupMembersComponent} from "./domain/settings/groups/group/members/members.component";
 import {ScimComponent} from "./domain/settings/scim/scim.component";
 import {EmailResolver} from "./resolvers/email.resolver";
+import {ConsentsResolver} from "./resolvers/consents.resolver";
+import {UserApplicationComponent} from "./domain/settings/users/user/applications/application/application.component";
 
 const routes: Routes = [
   { path: 'dashboard',
@@ -546,7 +550,13 @@ const routes: Routes = [
             component: UserComponent,
             resolve: {
               user: UserResolver
-            }
+            },
+            children: [
+              { path: '', redirectTo: 'profile', pathMatch: 'full' },
+              { path: 'profile', component: UserProfileComponent },
+              { path: 'applications', component: UserApplicationsComponent, resolve: {consents: ConsentsResolver}},
+              { path: 'applications/:clientId', component: UserApplicationComponent, resolve: {consents: ConsentsResolver}},
+            ]
           },
           { path: 'groups', component: GroupsComponent,
             resolve: {
diff --git a/gravitee-am-ui/src/app/app.module.ts b/gravitee-am-ui/src/app/app.module.ts
index d98b20f34e..4e4cb9c06d 100644
--- a/gravitee-am-ui/src/app/app.module.ts
+++ b/gravitee-am-ui/src/app/app.module.ts
@@ -141,6 +141,9 @@ import { UsersComponent } from './domain/settings/users/users.component';
 import { UserComponent } from './domain/settings/users/user/user.component';
 import { UserCreationComponent } from './domain/settings/users/creation/user-creation.component';
 import { UserClaimComponent } from './domain/settings/users/creation/user-claim.component';
+import { UserProfileComponent } from './domain/settings/users/user/profile/profile.component';
+import { UserApplicationsComponent } from './domain/settings/users/user/applications/applications.component';
+import { UserApplicationComponent } from './domain/settings/users/user/applications/application/application.component';
 import { UserService} from "./services/user.service";
 import { UsersResolver } from "./resolvers/users.resolver";
 import { UserResolver } from "./resolvers/user.resolver";
@@ -174,6 +177,7 @@ import { EmailService } from "./services/email.service";
 import { EmailResolver } from "./resolvers/email.resolver";
 import { SelectClientsComponent } from "./domain/components/clients/select-clients.component";
 import { ClientScopeComponent } from "./domain/clients/client/scopes/client-scope.component";
+import { ConsentsResolver } from "./resolvers/consents.resolver";
 
 @NgModule({
   declarations: [
@@ -253,6 +257,9 @@ import { ClientScopeComponent } from "./domain/clients/client/scopes/client-scop
     UserComponent,
     UserCreationComponent,
     UserClaimComponent,
+    UserProfileComponent,
+    UserApplicationsComponent,
+    UserApplicationComponent,
     ScopeCreationComponent,
     ScopeComponent,
     MaterialFileComponent,
@@ -328,6 +335,7 @@ import { ClientScopeComponent } from "./domain/clients/client/scopes/client-scop
     GroupResolver,
     EmailService,
     EmailResolver,
+    ConsentsResolver,
     { provide: Http, useClass: HttpService }
   ],
   entryComponents: [
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.html b/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.html
new file mode 100644
index 0000000000..2dbdfa3576
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.html
@@ -0,0 +1,74 @@
+<!--
+
+    Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<div class="user-application-container" fxFlex="70">
+  <a [routerLink]="['..']"><small><< Back to authorized apps</small></a>
+  <div class="user-application-content">
+    <div fxLayout="row" style="align-items: center;">
+      <h1>{{clientId}}</h1>
+      <div fxFlex fxLayoutAlign="end">
+        <button *ngIf="canRevokeAccess();" style="height: 40px;" mat-stroked-button color="warn" (click)="revokeApplication($event)">Revoke access</button>
+      </div>
+    </div>
+    <ngx-datatable class="material"
+                   [columnMode]="'flex'"
+                   [headerHeight]="40"
+                   [footerHeight]="40"
+                   [rowHeight]="50"
+                   [rows]='consents'
+                   [sorts]="[{prop: 'status', dir: 'desc'}]"
+                   [rowClass]="getRowClass">
+      <ngx-datatable-column>
+        <ng-template let-row="row" ngx-datatable-cell-template>
+          <mat-icon [ngStyle]="{'color': row.status === 'approved' ? 'green' : 'orange'}"  matTooltip="{{row.status === 'approved' ? 'Permission approved' : 'Permission denied'}}">
+            {{row.status === 'approved' ? 'done' : 'not_interested'}}
+          </mat-icon>
+        </ng-template>
+      </ngx-datatable-column>
+      <ngx-datatable-column name="Permission" [flexGrow]="2">
+        <ng-template let-row="row" ngx-datatable-cell-template>
+          {{row.scopeEntity.key}} | <small>{{row.scopeEntity.name}}</small> | <small>{{row.scopeEntity.description}}</small>
+        </ng-template>
+      </ngx-datatable-column>
+      <ngx-datatable-column name="Approval date" [flexGrow]="1">
+        <ng-template let-row="row" ngx-datatable-cell-template>
+          {{row.updatedAt | humanDate}}
+        </ng-template>
+      </ngx-datatable-column>
+      <ngx-datatable-column name="Expires at" [flexGrow]="1">
+        <ng-template let-row="row" ngx-datatable-cell-template>
+          <span>{{row.expiresAt | humanDate}}</span>
+        </ng-template>
+      </ngx-datatable-column>
+      <ngx-datatable-column [flexGrow]="1">
+        <ng-template let-row="row" ngx-datatable-cell-template>
+          <div fxLayout="row" class="user-application-actions">
+            <button *ngIf="row.status === 'approved'" mat-icon-button (click)="revokeConsent($event, row);"><mat-icon matTooltip="Revoke access">highlight_off</mat-icon></button>
+          </div>
+        </ng-template>
+      </ngx-datatable-column>
+    </ngx-datatable>
+  </div>
+</div>
+<div class="user-application-description" fxFlex>
+  <h3>Authorized application</h3>
+  <div class="user-application-description-content">
+    <p>
+      Application act on behalf the user to access data based on the permissions the user grant them.
+    </p>
+  </div>
+</div>
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.scss b/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.scss
new file mode 100644
index 0000000000..4fb93d8f79
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.scss
@@ -0,0 +1,78 @@
+.user-application-container {
+  a {
+    color: #006CAB;
+    text-decoration: none;
+    &:hover {
+      color: #2196f3;
+      text-decoration: underline;
+    }
+  }
+  .user-application-content {
+    margin-top: 10px;
+    padding: 0px 20px 20px 20px;
+    border-radius: 2px;
+    border: 1px solid rgb(226, 229, 231);
+
+    h1 {
+      font-weight: 400;
+    }
+
+    ngx-datatable {
+      font-size: 14px;
+      box-shadow: none;
+
+      .row-disabled {
+        background: #f1f1f1;
+      }
+    }
+
+    .user-application-actions {
+      margin-left: -10px;
+
+      button, a {
+        top: -11px;
+        color: #f44336;
+      }
+    }
+  }
+}
+
+.user-application-description {
+  margin-left: 20px;
+  h3 {
+    font-weight: 400;
+  }
+  .user-application-description-content {
+    border-top: 1px solid rgb(226, 229, 231);
+    p {
+      margin-top: 20px;
+      font-size: 14px;
+    }
+    small {
+      font-size: 12px;
+      color: rgba(0, 0, 0, 0.38);
+    }
+  }
+}
+
+:host {
+  /deep/ .ngx-datatable.material .datatable-footer {
+    display: none;
+  }
+
+  /deep/ .ngx-datatable.fixed-header .datatable-header .datatable-header-inner .datatable-header-cell {
+    &:first-child {
+      padding-left: 42px;
+    }
+  }
+
+  /deep/ .row-disabled {
+    background: #f1f1f1;
+    border: 1px solid #fafafa;
+    datatable-body-cell {
+      color: grey !important;
+      font-size: 85% !important;
+      font-style: italic !important;
+    }
+  }
+}
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.spec.ts b/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.spec.ts
new file mode 100644
index 0000000000..c5267d3fae
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UserApplicationComponent } from './application.component';
+
+describe('UserApplicationComponent', () => {
+  let component: UserApplicationComponent;
+  let fixture: ComponentFixture<UserApplicationComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ UserApplicationComponent ]
+    })
+      .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(UserApplicationComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should be created', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.ts b/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.ts
new file mode 100644
index 0000000000..480e530059
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, OnInit } from '@angular/core';
+import {ActivatedRoute, Router} from "@angular/router";
+import {SnackbarService} from "../../../../../../services/snackbar.service";
+import {DialogService} from "../../../../../../services/dialog.service";
+import {UserService} from "../../../../../../services/user.service";
+import * as _ from 'lodash';
+
+@Component({
+  selector: 'app-user-application',
+  templateUrl: './application.component.html',
+  styleUrls: ['./application.component.scss']
+})
+export class UserApplicationComponent implements OnInit {
+
+  private domainId: string;
+  private userId: string;
+  clientId: string;
+  consents: any[];
+
+  constructor(private route: ActivatedRoute,
+              private router: Router,
+              private snackbarService: SnackbarService,
+              private dialogService: DialogService,
+              private userService: UserService) {
+  }
+
+  ngOnInit() {
+    this.domainId = this.route.snapshot.parent.parent.parent.params['domainId'];
+    this.userId = this.route.snapshot.parent.params['userId'];
+    this.clientId = this.route.snapshot.params['clientId'];
+    this.consents = this.route.snapshot.data['consents'];
+  }
+
+  revokeApplication(event) {
+    event.preventDefault();
+    this.dialogService
+      .confirm('Revoke access', 'Are you sure you want to revoke application access ?')
+      .subscribe(res => {
+        if (res) {
+          this.userService.revokeConsents(this.domainId, this.userId, this.clientId).subscribe(response => {
+            this.snackbarService.open('Access for application '+ this.clientId + ' revoked');
+            this.router.navigate(['/domains', this.domainId, 'settings', 'users', this.userId, 'applications']);
+          });
+        }
+      });
+  }
+
+  revokeConsent(event, consent) {
+    console.log(consent);
+    event.preventDefault();
+    this.dialogService
+      .confirm('Revoke access', 'Are you sure you want to revoke this permission ?')
+      .subscribe(res => {
+        if (res) {
+          this.userService.revokeConsent(this.domainId, this.userId, consent.id).subscribe(response => {
+            this.snackbarService.open('Permission '+ consent.scopeEntity.name + ' revoked');
+            if (this.consents.length === 1) {
+              this.router.navigate(['/domains', this.domainId, 'settings', 'users', this.userId, 'applications']);
+            } else {
+              this.loadConsents();
+            }
+          });
+        }
+      });
+  }
+
+  loadConsents() {
+    this.userService.consents(this.domainId, this.userId, this.clientId).map(res => res.json()).subscribe(consents => {
+      this.consents = consents;
+      this.consents = [...this.consents];
+    });
+  }
+
+  canRevokeAccess() {
+    return _.find(this.consents, {status: 'approved'})
+  }
+
+  getRowClass(row) {
+    return {
+      'row-disabled': row.status !== 'approved'
+    };
+  }
+
+}
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.html b/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.html
new file mode 100644
index 0000000000..bed2e2f4f0
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.html
@@ -0,0 +1,75 @@
+<!--
+
+    Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<div class="user-applications-container">
+    <div class="user-applications-content" fxFlex="70">
+      <div fxLayout="row" *ngIf="!isEmpty">
+        <div>You have interacted with <b>{{appConsents.length}}</b> application(s) to access to your account.</div>
+        <div>
+          <button *ngIf="canRevokeAllAccess()" style="margin-left: 20px; height: 40px; color: #f44336;" mat-stroked-button color="warn" (click)="revokeAll($event)">Revoke all</button>
+        </div>
+      </div>
+      <ngx-datatable *ngIf="!isEmpty" class="material"
+                     [columnMode]="'flex'"
+                     [headerHeight]="40"
+                     [footerHeight]="40"
+                     [rowHeight]="50"
+                     [rows]='appConsents'
+                     [sorts]="[{prop: 'enabled', dir: 'desc'}]"
+                     [rowClass]="rowClass">
+        <ngx-datatable-column>
+          <ng-template let-row="row" ngx-datatable-cell-template>
+            <mat-icon [ngStyle]="{'color': canRevokeAccessForClient(row.clientId) ? 'green' : 'orange'}"  matTooltip="{{canRevokeAccessForClient(row.clientId) ? 'Application approved' : 'Application denied'}}">
+              {{canRevokeAccessForClient(row.clientId) ? 'done' : 'not_interested'}}
+            </mat-icon>
+          </ng-template>
+        </ngx-datatable-column>
+        <ngx-datatable-column name="Application" [flexGrow]="2">
+          <ng-template let-row="row" ngx-datatable-cell-template>
+            {{row.clientId}}
+          </ng-template>
+        </ngx-datatable-column>
+        <ngx-datatable-column name="Last interaction" [flexGrow]="1">
+          <ng-template let-row="row" ngx-datatable-cell-template>
+            {{row.updatedAt | humanDate}}
+          </ng-template>
+        </ngx-datatable-column>
+        <ngx-datatable-column [flexGrow]="1">
+          <ng-template let-row="row" ngx-datatable-cell-template>
+            <div fxLayout="row" class="forms-actions">
+              <a mat-icon-button [routerLink]="[row.clientId]"><mat-icon matTooltip="More info">more_horiz</mat-icon></a>
+              <button *ngIf="canRevokeAccessForClient(row.clientId)" mat-icon-button (click)="revoke($event, row)"><mat-icon style="color: #f44336;" matTooltip="Revoke application">highlight_off</mat-icon></button>
+            </div>
+          </ng-template>
+        </ngx-datatable-column>
+      </ngx-datatable>
+
+      <app-emptystate *ngIf="isEmpty"
+                      [message]="'No authorized applications'"
+                      [subMessage]="'You have no applications authorized to access your account.'"
+                      [icon]="'apps'"></app-emptystate>
+    </div>
+    <div class="user-applications-description" fxFlex>
+      <h3>Authorized applications</h3>
+      <div class="user-applications-description-content">
+        <p>
+          Applications act on behalf the user to access data based on the permissions the user grant them.
+        </p>
+      </div>
+    </div>
+</div>
+
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.scss b/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.scss
new file mode 100644
index 0000000000..e5a586524a
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.scss
@@ -0,0 +1,52 @@
+.user-applications-content {
+  ngx-datatable {
+    font-size: 14px;
+    box-shadow: none;
+  }
+  button, a {
+    top: -11px;
+    color: grey;
+  }
+}
+
+.user-applications-description {
+  margin-left: 20px;
+  h3 {
+    font-weight: 400;
+    border-bottom: 1px solid rgb(226, 229, 231);
+    padding-bottom: 20px;
+  }
+  .user-applications-description-content {
+    p {
+      margin-top: 20px;
+      font-size: 14px;
+    }
+    small {
+      font-size: 12px;
+      color: rgba(0, 0, 0, 0.38);
+    }
+    margin-bottom: 50px;
+  }
+}
+
+:host {
+  /deep/ .ngx-datatable.material .datatable-footer {
+    display: none;
+  }
+
+  /deep/ .ngx-datatable.fixed-header .datatable-header .datatable-header-inner .datatable-header-cell {
+    &:first-child {
+      padding-left: 42px;
+    }
+  }
+
+  /deep/ .row-disabled {
+    background: #f1f1f1;
+    border: 1px solid #fafafa;
+    datatable-body-cell {
+      color: grey !important;
+      font-size: 85% !important;
+      font-style: italic !important;
+    }
+  }
+}
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.spec.ts b/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.spec.ts
new file mode 100644
index 0000000000..4a0693106f
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UserApplicationsComponent } from './applications.component';
+
+describe('UserApplicationsComponent', () => {
+  let component: UserApplicationsComponent;
+  let fixture: ComponentFixture<UserApplicationsComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ UserApplicationsComponent ]
+    })
+      .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(UserApplicationsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should be created', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.ts b/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.ts
new file mode 100644
index 0000000000..be6f0aee9f
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/applications/applications.component.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from "@angular/router";
+import { SnackbarService } from "../../../../../services/snackbar.service";
+import { DialogService } from "../../../../../services/dialog.service";
+import { UserService } from "../../../../../services/user.service";
+import * as _ from 'lodash';
+
+@Component({
+  selector: 'app-user-applications',
+  templateUrl: './applications.component.html',
+  styleUrls: ['./applications.component.scss']
+})
+export class UserApplicationsComponent implements OnInit {
+  private domainId: string;
+  private user: any;
+  private consents: any[];
+  private appConsentsGrouped: any = {};
+  appConsents: any[];
+
+  constructor(private route: ActivatedRoute,
+              private router: Router,
+              private snackbarService: SnackbarService,
+              private dialogService: DialogService,
+              private userService: UserService) {
+  }
+
+  ngOnInit() {
+    this.domainId = this.route.snapshot.parent.parent.parent.params['domainId'];
+    this.user = this.route.snapshot.parent.data['user'];
+    this.consents = _.sortBy(this.route.snapshot.data['consents'], 'updatedAt').reverse();
+    this.appConsentsGrouped  = _.groupBy(this.consents, 'clientId');
+    this.appConsents = _.sortedUniqBy(this.consents, 'clientId');
+  }
+
+  get isEmpty() {
+    return !this.consents || this.consents.length == 0;
+  }
+
+  revoke(event, consent) {
+    event.preventDefault();
+    this.dialogService
+      .confirm('Revoke access', 'Are you sure you want to revoke authorization ?')
+      .subscribe(res => {
+        if (res) {
+          this.userService.revokeConsents(this.domainId, this.user.id, consent.clientId).subscribe(response => {
+            this.snackbarService.open('Access for application '+ consent.clientId + ' revoked');
+            this.loadConsents();
+          });
+        }
+      });
+  }
+
+  revokeAll(event) {
+    event.preventDefault();
+    this.dialogService
+      .confirm('Revoke access', 'Are you sure you want to revoke access to all these applications ?')
+      .subscribe(res => {
+        if (res) {
+          this.userService.revokeConsents(this.domainId, this.user.id, null).subscribe(response => {
+            this.snackbarService.open('Access revoked');
+            this.loadConsents();
+          });
+        }
+      });
+  }
+
+  loadConsents() {
+    this.userService.consents(this.domainId, this.user.id, null).map(res => res.json()).subscribe(consents => {
+      this.consents = consents;
+      this.appConsents = _.uniqBy(this.consents, 'clientId');
+    });
+  }
+
+  rowClass = (row) => {
+    return {
+      'row-disabled': !this.canRevokeAccessForClient(row.clientId)
+    };
+  }
+
+  canRevokeAccessForClient(clientId) {
+    return _.find(this.appConsentsGrouped[clientId], {status: 'approved'});
+  }
+
+  canRevokeAllAccess() {
+    return _.find(this.consents, {status: 'approved'});
+  }
+}
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.html b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.html
new file mode 100644
index 0000000000..3829207186
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.html
@@ -0,0 +1,141 @@
+<!--
+
+    Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<div class="user-profile-container">
+  <div class="user-profile-content">
+    <div class="user-profile-claims" fxFlex="70">
+      <form (ngSubmit)="update()" fxLayout="column" #userForm="ngForm">
+        <div fxLayout="row" *ngIf="user.username">
+          <mat-form-field>
+            <input matInput placeholder="Username" disabled name="username" [value]="user.username" required>
+          </mat-form-field>
+        </div>
+        <div *ngIf="editMode()" fxLayout="column" style="margin-bottom: 20px;">
+          <mat-slide-toggle
+            (change)="enableUser($event)"
+            [checked]="isUserEnabled()">
+            Enable User
+          </mat-slide-toggle>
+          <mat-hint style="font-size: 75%;">Enable or disable user to restrict application access.
+            By disabled the user, he will not be able to log to their applications.</mat-hint>
+        </div>
+        <div fxLayout="row">
+          <mat-form-field *ngIf="user.firstName" style="margin-right: 20px;">
+            <input matInput type="text" placeholder="First name" [disabled]="!editMode()" #firstName="ngModel" name="firstName" [(ngModel)]="user.firstName" required>
+            <mat-hint *ngIf="editMode()">The first name of the User.</mat-hint>
+            <mat-error *ngIf="firstName.errors?.required">Enter first name</mat-error>
+          </mat-form-field>
+          <mat-form-field *ngIf="user.lastName">
+            <input matInput type="text" placeholder="Last name" [disabled]="!editMode()" #lastName="ngModel" name="lastName" [(ngModel)]="user.lastName" required>
+            <mat-hint *ngIf="editMode()">The last name of the User.</mat-hint>
+            <mat-error *ngIf="lastName.errors?.required">Enter last name</mat-error>
+          </mat-form-field>
+        </div>
+        <div fxLayout="row" *ngIf="user.email">
+          <mat-form-field>
+            <input matInput placeholder="Email" [disabled]="!editMode()" #email="ngModel" name="email" [(ngModel)]="user.email" [email]="user.email !== ''" required>
+            <mat-hint *ngIf="editMode()">Email address for the User.</mat-hint>
+            <mat-error *ngIf="email.errors?.required">Enter email</mat-error>
+            <mat-error *ngIf="user.email && email.errors?.email">Not a valid email</mat-error>
+          </mat-form-field>
+        </div>
+        <div fxLayout="column" *ngIf="editMode() && user.preRegistration && !user.registrationCompleted" class="additional-information">
+          <div fxLayout="row" style="color: orange;"><mat-icon>warning</mat-icon><h4 style="margin-top: 5px;">User does not complete registration</h4></div>
+          <span>An email will be sent to the user asking to complete his account.</span>
+          <a (click)="resendConfirmationRegistration()">Resend confirmation e-mail</a>
+        </div>
+        <div *ngIf="editMode()">
+          <app-select-clients [selectedClient]="user.clientEntity" (onSelectClient)="onClientSelectionChanged($event)" (onRemoveClient)="onClientDeleted($event)"></app-select-clients>
+        </div>
+        <div *ngIf="editMode()" fxLayout="column" class="additional-information">
+          <form (ngSubmit)="resetPassword()" #passwordForm="ngForm" fxLayout="row" style="background: #f1f1f1; border: none;">
+            <mat-form-field style="margin-right: 20px" fxFlex="75">
+              <input matInput type="password" name="password" placeholder="Password" required [(ngModel)]="password"/>
+              <mat-hint>User's password</mat-hint>
+            </mat-form-field>
+            <button style="height:36px; margin-top: 20px;" mat-raised-button color="primary" [disabled]="!passwordForm.valid || passwordForm.pristine" type="submit">RESET PASSWORD</button>
+          </form>
+        </div>
+        <div fxLayout="column" class="additional-information" *ngIf="!isEmptyObject(user.additionalInformation) || editMode()">
+          <h4>Additional information</h4>
+          <span>Custom information about the user. These details are available in the user's profile.</span>
+          <a (click)="addDynamicComponent()" *ngIf="editMode()">(+) add user attribute</a>
+          <template #dynamic></template>
+          <div *ngFor="let keyValuePair of userAdditionalInformation | mapToIterable">
+            <div *ngIf="keyValuePair.value" fxLayout="row">
+              <mat-form-field style="margin-right: 20px;" fxFlex="40">
+                <input matInput type="text" name="claimName" [value]="keyValuePair.key" disabled>
+              </mat-form-field>
+              <mat-form-field>
+                <input matInput type="text" name="claimValue" [value]="keyValuePair.value" (change)="formChanged = true;" [disabled]="!editMode()">
+              </mat-form-field>
+              <button mat-icon-button *ngIf="editMode()" (click)="removeExistingClaim(keyValuePair.key, $event)"><mat-icon>clear</mat-icon></button>
+            </div>
+          </div>
+        </div>
+
+        <div fxLayout="row" fxLayoutAlign="end" style="margin-top: 50px;" *ngIf="editMode()">
+          <a mat-button [routerLink]="['..']" style="margin-right: 20px;">CANCEL</a>
+          <button mat-raised-button [disabled]="(!userForm.valid || userForm.pristine) && !formChanged" type="submit">UPDATE</button>
+        </div>
+      </form>
+      <div class="user-profile-settings-delete-zone" fxLayout="column" *ngIf="editMode()">
+        <p>This operation cannot be undone.</p>
+        User will be deleted and will no longer be able to sign in to applications.
+        <div class="user-profile-settings-delete-button" fxLayout="row">
+          <span fxFlex></span>
+          <button mat-raised-button color="warn" (click)="delete($event)">DELETE</button>
+        </div>
+      </div>
+    </div>
+    <div class="user-profile-activities" fxFlex>
+      <h3>User activities</h3>
+      <div class="user-profile-activities-content">
+        <mat-list>
+          <mat-list-item>
+            <mat-icon mat-list-icon>trending_up</mat-icon>
+            <h4 matLine>Logins count</h4>
+            <p matLine>{{user.loginsCount}}</p>
+          </mat-list-item>
+          <mat-list-item>
+            <mat-icon mat-list-icon>alarm</mat-icon>
+            <h4 matLine>Last login</h4>
+            <p matLine>
+              <span *ngIf="!(user.loggedAt)">Never logged in</span>
+              <span *ngIf="(user.loggedAt)">{{user.loggedAt | humanDate}}</span>
+            </p>
+          </mat-list-item>
+          <mat-list-item>
+            <mat-icon mat-list-icon>alarm_on</mat-icon>
+            <h4 matLine>Created at</h4>
+            <p matLine>{{user.createdAt | humanDate}}</p>
+          </mat-list-item>
+          <mat-list-item>
+            <mat-icon mat-list-icon>dock</mat-icon>
+            <h4 matLine>Last client</h4>
+            <p matLine>{{displayClientName()}}</p>
+          </mat-list-item>
+          <mat-list-item>
+            <mat-icon mat-list-icon>device_hub</mat-icon>
+            <h4 matLine>Identity provider</h4>
+            <p matLine>{{user.source}}</p>
+          </mat-list-item>
+        </mat-list>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.scss b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.scss
new file mode 100644
index 0000000000..fdd9bfeae9
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.scss
@@ -0,0 +1,55 @@
+.user-profile-container {
+  .user-profile-content {
+    form {
+      padding: 20px;
+      background: #fafafa;
+      border-radius: 2px;
+      border: 1px solid rgb(226, 229, 231);
+    }
+    .additional-information {
+      margin-top: 20px;
+      padding: 10px;
+      background-color: #f1f1f1;
+      h4 {
+        margin: 0;
+        font-weight: 400;
+      }
+      span {
+        font-size: 75%;
+        color: rgba(0, 0, 0, 0.54);
+      }
+      a {
+        cursor: pointer;
+        font-size: 75%;
+        color: #2196f3;
+      }
+    }
+    .user-profile-settings-delete-zone {
+      color: #f44336;
+      font-size: 75%;
+      margin-top: 20px;
+      padding: 20px;
+      border-radius: 2px;
+      border: 1px solid #e2e5e7;
+      .user-profile-settings-delete-button {
+        margin-top: 20px;
+      }
+    }
+    .user-profile-activities {
+      margin-left: 20px;
+      h3 {
+        font-weight: 400;
+      }
+      .user-profile-activities-content {
+        border-top: 1px solid rgb(226, 229, 231);
+        p {
+          font-size: 12px;
+          color: rgba(0, 0, 0, 0.38);
+        }
+        mat-icon {
+          color: grey;
+        }
+      }
+    }
+  }
+}
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.spec.ts b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.spec.ts
new file mode 100644
index 0000000000..59653266b6
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UserProfileComponent } from './profile.component';
+
+describe('UserProfileComponent', () => {
+  let component: UserProfileComponent;
+  let fixture: ComponentFixture<UserProfileComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ UserProfileComponent ]
+    })
+      .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(UserProfileComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should be created', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts
new file mode 100644
index 0000000000..054fc835ee
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, ComponentFactoryResolver, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
+import { ActivatedRoute, Router } from "@angular/router";
+import { BreadcrumbService } from "../../../../../../libraries/ng2-breadcrumb/components/breadcrumbService";
+import { SnackbarService } from "../../../../../services/snackbar.service";
+import { DialogService } from "../../../../../services/dialog.service";
+import { UserService } from "../../../../../services/user.service";
+import { AppConfig } from "../../../../../../config/app.config";
+import { UserClaimComponent } from "../../creation/user-claim.component";
+import * as _ from 'lodash';
+
+@Component({
+  selector: 'app-user-profile',
+  templateUrl: './profile.component.html',
+  styleUrls: ['./profile.component.scss']
+})
+export class UserProfileComponent implements OnInit {
+  private domainId: string;
+  private adminContext: boolean;
+  @ViewChild('userForm') form: any;
+  @ViewChild('passwordForm') passwordForm: any;
+  @ViewChild('dynamic', { read: ViewContainerRef }) viewContainerRef: ViewContainerRef;
+  user: any;
+  userClaims: any = {};
+  userAdditionalInformation: any = {};
+  password: any;
+  formChanged: boolean = false;
+
+  constructor(private route: ActivatedRoute,
+              private router: Router,
+              private breadcrumbService: BreadcrumbService,
+              private snackbarService: SnackbarService,
+              private dialogService: DialogService,
+              private userService: UserService,
+              private factoryResolver: ComponentFactoryResolver) { }
+
+  ngOnInit() {
+    this.domainId = this.route.snapshot.parent.parent.params['domainId'];
+    if (this.router.routerState.snapshot.url.startsWith('/settings')) {
+      this.domainId = AppConfig.settings.authentication.domainId;
+      this.adminContext = true;
+    }
+    this.user = this.route.snapshot.parent.data['user'];
+    this.userAdditionalInformation = Object.assign({}, this.user.additionalInformation);
+  }
+
+  initBreadcrumb() {
+    this.breadcrumbService.addFriendlyNameForRouteRegex('/domains/'+this.domainId+'/settings/users/'+this.user.id+'$', this.user.username);
+  }
+
+  update() {
+    // set additional information
+    if (this.userClaims && Object.keys(this.userClaims).length > 0) {
+      let additionalInformation = this.userAdditionalInformation;
+      _.each(this.userClaims, function(item) {
+        additionalInformation[item.claimName] = item.claimValue;
+      });
+      this.user.additionalInformation = additionalInformation;
+    }
+
+    this.userService.update(this.domainId, this.user.id, this.user).map(res => res.json()).subscribe(data => {
+      this.user = data;
+      this.userAdditionalInformation = Object.assign({}, this.user.additionalInformation);
+      this.userClaims = {};
+      this.viewContainerRef.clear();
+      this.initBreadcrumb();
+      this.form.reset(this.user);
+      this.formChanged = false;
+      this.snackbarService.open("User updated");
+    });
+  }
+
+  delete(event) {
+    event.preventDefault();
+    this.dialogService
+      .confirm('Delete User', 'Are you sure you want to delete this user ?')
+      .subscribe(res => {
+        if (res) {
+          this.userService.delete(this.domainId, this.user.id).subscribe(response => {
+            this.snackbarService.open('User '+ this.user.username + ' deleted');
+            this.router.navigate(['/domains', this.domainId, 'settings', 'users']);
+          });
+        }
+      });
+  }
+
+  resendConfirmationRegistration() {
+    this.userService.resendRegistrationConfirmation(this.domainId, this.user.id).map(res => res.json()).subscribe(() => {
+      this.snackbarService.open("Email sent");
+    });
+  }
+
+  resetPassword() {
+    this.userService.resetPassword(this.domainId, this.user.id, this.password).map(res => res.json()).subscribe(() => {
+      this.password = null;
+      this.passwordForm.reset();
+      // reset the errors of all the controls
+      for (let name in this.passwordForm.controls) {
+        this.passwordForm.controls[name].setErrors(null);
+      }
+      this.snackbarService.open("Password reset");
+    });
+  }
+
+  editMode() {
+    return this.user.internal && !this.adminContext;
+  }
+
+  isEmptyObject(obj) {
+    return (obj && (Object.keys(obj).length === 0));
+  }
+
+  enableUser(event) {
+    this.user.enabled = event.checked;
+    this.formChanged = true;
+  }
+
+  isUserEnabled() {
+    return this.user.enabled;
+  }
+
+  addDynamicComponent() {
+    const factory = this.factoryResolver.resolveComponentFactory(UserClaimComponent);
+    const component = this.viewContainerRef.createComponent(factory);
+
+    let that = this;
+    component.instance.addClaimChange.subscribe(claim => {
+      if (claim.name && claim.value) {
+        that.userClaims[claim.id] = {'claimName': claim.name, 'claimValue': claim.value};
+        this.formChanged = true;
+      }
+    });
+
+    component.instance.removeClaimChange.subscribe(claim => {
+      delete that.userClaims[claim.id];
+      that.viewContainerRef.remove(that.viewContainerRef.indexOf(component.hostView));
+      if (claim.name && claim.value) {
+        that.snackbarService.open('Claim ' + claim.name + ' deleted');
+        this.formChanged = true;
+      }
+    });
+  }
+
+  removeExistingClaim(claim, event) {
+    event.preventDefault();
+    delete this.user.additionalInformation[claim];
+    this.userAdditionalInformation = Object.assign({}, this.user.additionalInformation);
+    this.formChanged = true;
+  }
+
+  onClientSelectionChanged(event) {
+    this.user.client = event.id;
+    this.formChanged = true;
+  }
+
+  onClientDeleted(event) {
+    this.user.client = null;
+    this.formChanged = true;
+  }
+
+  displayClientName() {
+    if (this.user.clientEntity != null) {
+      return (this.user.clientEntity.clientName) ? this.user.clientEntity.clientName : this.user.clientEntity.clientId;
+    }
+    return this.user.client;
+  }
+}
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/user.component.html b/gravitee-am-ui/src/app/domain/settings/users/user/user.component.html
index 50e13ec683..8571791cc8 100644
--- a/gravitee-am-ui/src/app/domain/settings/users/user/user.component.html
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/user.component.html
@@ -17,126 +17,16 @@
 -->
 <div class="user-container">
   <h1>{{user.username}}</h1>
+  <nav mat-tab-nav-bar>
+    <a mat-tab-link
+       *ngFor="let link of navLinks"
+       [routerLink]="link.href"
+       routerLinkActive #rla="routerLinkActive"
+       [active]="rla.isActive">
+      {{link.label}}
+    </a>
+  </nav>
   <div class="user-content">
-    <div class="user-claims" fxFlex="70">
-      <form (ngSubmit)="update()" fxLayout="column" #userForm="ngForm">
-        <div fxLayout="row" *ngIf="user.username">
-          <mat-form-field>
-            <input matInput placeholder="Username" disabled name="username" [value]="user.username" required>
-          </mat-form-field>
-        </div>
-        <div *ngIf="editMode()" fxLayout="column" style="margin-bottom: 20px;">
-          <mat-slide-toggle
-            (change)="enableUser($event)"
-            [checked]="isUserEnabled()">
-            Enable User
-          </mat-slide-toggle>
-          <mat-hint style="font-size: 75%;">Enable or disable user to restrict application access.
-            By disabled the user, he will not be able to log to their applications.</mat-hint>
-        </div>
-        <div fxLayout="row">
-          <mat-form-field *ngIf="user.firstName" style="margin-right: 20px;">
-            <input matInput type="text" placeholder="First name" [disabled]="!editMode()" #firstName="ngModel" name="firstName" [(ngModel)]="user.firstName" required>
-            <mat-hint *ngIf="editMode()">The first name of the User.</mat-hint>
-            <mat-error *ngIf="firstName.errors?.required">Enter first name</mat-error>
-          </mat-form-field>
-          <mat-form-field *ngIf="user.lastName">
-            <input matInput type="text" placeholder="Last name" [disabled]="!editMode()" #lastName="ngModel" name="lastName" [(ngModel)]="user.lastName" required>
-            <mat-hint *ngIf="editMode()">The last name of the User.</mat-hint>
-            <mat-error *ngIf="lastName.errors?.required">Enter last name</mat-error>
-          </mat-form-field>
-        </div>
-        <div fxLayout="row" *ngIf="user.email">
-          <mat-form-field>
-            <input matInput placeholder="Email" [disabled]="!editMode()" #email="ngModel" name="email" [(ngModel)]="user.email" [email]="user.email !== ''" required>
-            <mat-hint *ngIf="editMode()">Email address for the User.</mat-hint>
-            <mat-error *ngIf="email.errors?.required">Enter email</mat-error>
-            <mat-error *ngIf="user.email && email.errors?.email">Not a valid email</mat-error>
-          </mat-form-field>
-        </div>
-        <div fxLayout="column" *ngIf="editMode() && user.preRegistration && !user.registrationCompleted" class="additional-information">
-          <div fxLayout="row" style="color: orange;"><mat-icon>warning</mat-icon><h4 style="margin-top: 5px;">User does not complete registration</h4></div>
-          <span>An email will be sent to the user asking to complete his account.</span>
-          <a (click)="resendConfirmationRegistration()">Resend confirmation e-mail</a>
-        </div>
-        <div *ngIf="editMode()">
-          <app-select-clients [selectedClient]="user.clientEntity" (onSelectClient)="onClientSelectionChanged($event)" (onRemoveClient)="onClientDeleted($event)"></app-select-clients>
-        </div>
-        <div *ngIf="editMode()" fxLayout="column" class="additional-information">
-          <form (ngSubmit)="resetPassword()" #passwordForm="ngForm" fxLayout="row" style="background: #f1f1f1; border: none;">
-            <mat-form-field style="margin-right: 20px" fxFlex="75">
-              <input matInput type="password" name="password" placeholder="Password" required [(ngModel)]="password"/>
-              <mat-hint>User's password</mat-hint>
-            </mat-form-field>
-            <button style="height:36px; margin-top: 20px;" mat-raised-button color="primary" [disabled]="!passwordForm.valid || passwordForm.pristine" type="submit">RESET PASSWORD</button>
-          </form>
-        </div>
-        <div fxLayout="column" class="additional-information" *ngIf="!isEmptyObject(user.additionalInformation) || editMode()">
-          <h4>Additional information</h4>
-          <span>Custom information about the user. These details are available in the user's profile.</span>
-          <a (click)="addDynamicComponent()" *ngIf="editMode()">(+) add user attribute</a>
-          <template #dynamic></template>
-          <div *ngFor="let keyValuePair of userAdditionalInformation | mapToIterable">
-            <div *ngIf="keyValuePair.value" fxLayout="row">
-              <mat-form-field style="margin-right: 20px;" fxFlex="40">
-                <input matInput type="text" name="claimName" [value]="keyValuePair.key" disabled>
-              </mat-form-field>
-              <mat-form-field>
-                <input matInput type="text" name="claimValue" [value]="keyValuePair.value" (change)="formChanged = true;" [disabled]="!editMode()">
-              </mat-form-field>
-              <button mat-icon-button *ngIf="editMode()" (click)="removeExistingClaim(keyValuePair.key, $event)"><mat-icon>clear</mat-icon></button>
-            </div>
-          </div>
-        </div>
-
-        <div fxLayout="row" fxLayoutAlign="end" style="margin-top: 50px;" *ngIf="editMode()">
-          <a mat-button [routerLink]="['..']" style="margin-right: 20px;">CANCEL</a>
-          <button mat-raised-button [disabled]="(!userForm.valid || userForm.pristine) && !formChanged" type="submit">UPDATE</button>
-        </div>
-      </form>
-      <div class="user-settings-delete-zone" fxLayout="column" *ngIf="editMode()">
-        <p>This operation cannot be undone.</p>
-        User will be deleted and will no longer be able to sign in to applications.
-        <div class="user-settings-delete-button" fxLayout="row">
-          <span fxFlex></span>
-          <button mat-raised-button color="warn" (click)="delete($event)">DELETE</button>
-        </div>
-      </div>
-    </div>
-    <div class="user-activities" fxFlex>
-      <h3>User activities</h3>
-      <div class="user-activities-content">
-        <mat-list>
-          <mat-list-item>
-            <mat-icon mat-list-icon>trending_up</mat-icon>
-            <h4 matLine>Logins count</h4>
-            <p matLine>{{user.loginsCount}}</p>
-          </mat-list-item>
-          <mat-list-item>
-            <mat-icon mat-list-icon>alarm</mat-icon>
-            <h4 matLine>Last login</h4>
-            <p matLine>
-              <span *ngIf="!(user.loggedAt)">Never logged in</span>
-              <span *ngIf="(user.loggedAt)">{{user.loggedAt | humanDate}}</span>
-            </p>
-          </mat-list-item>
-          <mat-list-item>
-            <mat-icon mat-list-icon>alarm_on</mat-icon>
-            <h4 matLine>Created at</h4>
-            <p matLine>{{user.createdAt | humanDate}}</p>
-          </mat-list-item>
-          <mat-list-item>
-            <mat-icon mat-list-icon>dock</mat-icon>
-            <h4 matLine>Last client</h4>
-            <p matLine>{{displayClientName()}}</p>
-          </mat-list-item>
-          <mat-list-item>
-            <mat-icon mat-list-icon>device_hub</mat-icon>
-            <h4 matLine>Identity provider</h4>
-            <p matLine>{{user.source}}</p>
-          </mat-list-item>
-        </mat-list>
-      </div>
-    </div>
+    <router-outlet></router-outlet>
   </div>
 </div>
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/user.component.scss b/gravitee-am-ui/src/app/domain/settings/users/user/user.component.scss
index 3acfa3f75b..b2094c5b7d 100644
--- a/gravitee-am-ui/src/app/domain/settings/users/user/user.component.scss
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/user.component.scss
@@ -4,57 +4,6 @@
     font-weight: 400;
   }
   .user-content {
-    padding: 50px 0 20px;
-    form {
-      padding: 20px;
-      background: #fafafa;
-      border-radius: 2px;
-      border: 1px solid rgb(226, 229, 231);
-    }
-    .additional-information {
-      margin-top: 20px;
-      padding: 10px;
-      background-color: #f1f1f1;
-      h4 {
-        margin: 0;
-        font-weight: 400;
-      }
-      span {
-        font-size: 75%;
-        color: rgba(0, 0, 0, 0.54);
-      }
-      a {
-        cursor: pointer;
-        font-size: 75%;
-        color: #2196f3;
-      }
-    }
-    .user-settings-delete-zone {
-      color: #f44336;
-      font-size: 75%;
-      margin-top: 20px;
-      padding: 20px;
-      border-radius: 2px;
-      border: 1px solid #e2e5e7;
-      .user-settings-delete-button {
-        margin-top: 20px;
-      }
-    }
-    .user-activities {
-      margin-left: 20px;
-      h3 {
-        font-weight: 400;
-      }
-      .user-activities-content {
-        border-top: 1px solid rgb(226, 229, 231);
-        p {
-          font-size: 12px;
-          color: rgba(0, 0, 0, 0.38);
-        }
-        mat-icon {
-          color: grey;
-        }
-      }
-    }
+    padding: 25px 0 20px;
   }
 }
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/user.component.ts b/gravitee-am-ui/src/app/domain/settings/users/user/user.component.ts
index cafddc472b..a4475a834b 100644
--- a/gravitee-am-ui/src/app/domain/settings/users/user/user.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/user.component.ts
@@ -13,15 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, ComponentFactoryResolver, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from "@angular/router";
 import { BreadcrumbService } from "../../../../../libraries/ng2-breadcrumb/components/breadcrumbService";
-import { ActivatedRoute, Router } from "@angular/router";
-import { AppConfig } from "../../../../../config/app.config";
-import { SnackbarService} from "../../../../services/snackbar.service";
-import { UserService } from "../../../../services/user.service";
-import { DialogService } from "../../../../services/dialog.service";
-import { UserClaimComponent } from "../creation/user-claim.component";
-import * as _ from 'lodash';
 
 @Component({
   selector: 'app-user',
@@ -30,153 +24,23 @@ import * as _ from 'lodash';
 })
 export class UserComponent implements OnInit {
   private domainId: string;
-  private adminContext: boolean;
-  @ViewChild('userForm') form: any;
-  @ViewChild('passwordForm') passwordForm: any;
-  @ViewChild('dynamic', { read: ViewContainerRef }) viewContainerRef: ViewContainerRef;
   user: any;
-  userClaims: any = {};
-  userAdditionalInformation: any = {};
-  password: any;
-  formChanged: boolean = false;
+  navLinks: any = [
+    {'href': 'profile' , 'label': 'Profile'},
+    {'href': 'applications' , 'label': 'Authorized Apps'}
+  ];
 
-  constructor(private route: ActivatedRoute,
-              private router: Router,
-              private breadcrumbService: BreadcrumbService,
-              private snackbarService: SnackbarService,
-              private dialogService: DialogService,
-              private userService: UserService,
-              private factoryResolver: ComponentFactoryResolver) { }
+  constructor(private route: ActivatedRoute, private breadcrumbService: BreadcrumbService) {
+  }
 
   ngOnInit() {
     this.domainId = this.route.snapshot.parent.parent.params['domainId'];
-    if (this.router.routerState.snapshot.url.startsWith('/settings')) {
-      this.domainId = AppConfig.settings.authentication.domainId;
-      this.adminContext = true;
-    }
     this.user = this.route.snapshot.data['user'];
-    this.userAdditionalInformation = Object.assign({}, this.user.additionalInformation);
     this.initBreadcrumb();
   }
 
   initBreadcrumb() {
     this.breadcrumbService.addFriendlyNameForRouteRegex('/domains/'+this.domainId+'/settings/users/'+this.user.id+'$', this.user.username);
-  }
-
-  update() {
-    // set additional information
-    if (this.userClaims && Object.keys(this.userClaims).length > 0) {
-      let additionalInformation = this.userAdditionalInformation;
-      _.each(this.userClaims, function(item) {
-        additionalInformation[item.claimName] = item.claimValue;
-      });
-      this.user.additionalInformation = additionalInformation;
-    }
-
-    this.userService.update(this.domainId, this.user.id, this.user).map(res => res.json()).subscribe(data => {
-      this.user = data;
-      this.userAdditionalInformation = Object.assign({}, this.user.additionalInformation);
-      this.userClaims = {};
-      this.viewContainerRef.clear();
-      this.initBreadcrumb();
-      this.form.reset(this.user);
-      this.formChanged = false;
-      this.snackbarService.open("User updated");
-    });
-  }
-
-  delete(event) {
-    event.preventDefault();
-    this.dialogService
-      .confirm('Delete User', 'Are you sure you want to delete this user ?')
-      .subscribe(res => {
-        if (res) {
-          this.userService.delete(this.domainId, this.user.id).subscribe(response => {
-            this.snackbarService.open('User '+ this.user.username + ' deleted');
-            this.router.navigate(['/domains', this.domainId, 'settings', 'users']);
-          });
-        }
-      });
-  }
-
-  resendConfirmationRegistration() {
-    this.userService.resendRegistrationConfirmation(this.domainId, this.user.id).map(res => res.json()).subscribe(() => {
-      this.snackbarService.open("Email sent");
-    });
-  }
-
-  resetPassword() {
-    this.userService.resetPassword(this.domainId, this.user.id, this.password).map(res => res.json()).subscribe(() => {
-      this.password = null;
-      this.passwordForm.reset();
-      // reset the errors of all the controls
-      for (let name in this.passwordForm.controls) {
-        this.passwordForm.controls[name].setErrors(null);
-      }
-      this.snackbarService.open("Password reset");
-    });
-  }
-
-  editMode() {
-    return this.user.internal && !this.adminContext;
-  }
-
-  isEmptyObject(obj) {
-    return (obj && (Object.keys(obj).length === 0));
-  }
-
-  enableUser(event) {
-    this.user.enabled = event.checked;
-    this.formChanged = true;
-  }
-
-  isUserEnabled() {
-    return this.user.enabled;
-  }
-
-  addDynamicComponent() {
-    const factory = this.factoryResolver.resolveComponentFactory(UserClaimComponent);
-    const component = this.viewContainerRef.createComponent(factory);
-
-    let that = this;
-    component.instance.addClaimChange.subscribe(claim => {
-      if (claim.name && claim.value) {
-        that.userClaims[claim.id] = {'claimName': claim.name, 'claimValue': claim.value};
-        this.formChanged = true;
-      }
-    });
-
-    component.instance.removeClaimChange.subscribe(claim => {
-      delete that.userClaims[claim.id];
-      that.viewContainerRef.remove(that.viewContainerRef.indexOf(component.hostView));
-      if (claim.name && claim.value) {
-        that.snackbarService.open('Claim ' + claim.name + ' deleted');
-        this.formChanged = true;
-      }
-    });
-  }
-
-  removeExistingClaim(claim, event) {
-    event.preventDefault();
-    delete this.user.additionalInformation[claim];
-    this.userAdditionalInformation = Object.assign({}, this.user.additionalInformation);
-    this.formChanged = true;
-  }
-
-  onClientSelectionChanged(event) {
-    this.user.client = event.id;
-    this.formChanged = true;
-  }
-
-  onClientDeleted(event) {
-    this.user.client = null;
-    this.formChanged = true;
-  }
-
-  displayClientName() {
-    if (this.user.clientEntity != null) {
-      return (this.user.clientEntity.clientName) ? this.user.clientEntity.clientName : this.user.clientEntity.clientId;
-    }
-    return this.user.client;
+    this.breadcrumbService.addFriendlyNameForRouteRegex('/domains/'+this.domainId+'/settings/users/'+this.user.id+'/applications$', 'Authorized Apps');
   }
 }
diff --git a/gravitee-am-ui/src/app/resolvers/consents.resolver.spec.ts b/gravitee-am-ui/src/app/resolvers/consents.resolver.spec.ts
new file mode 100644
index 0000000000..8f8624dfb6
--- /dev/null
+++ b/gravitee-am-ui/src/app/resolvers/consents.resolver.spec.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { TestBed, inject } from '@angular/core/testing';
+
+import { ConsentsResolver } from './consents.resolver';
+
+describe('ConsentsResolver', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [ConsentsResolver]
+    });
+  });
+
+  it('should ...', inject([ConsentsResolver], (service: ConsentsResolver) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/gravitee-am-ui/src/app/resolvers/consents.resolver.ts b/gravitee-am-ui/src/app/resolvers/consents.resolver.ts
new file mode 100644
index 0000000000..718bf82792
--- /dev/null
+++ b/gravitee-am-ui/src/app/resolvers/consents.resolver.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Injectable } from '@angular/core';
+import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
+import { Observable } from "rxjs";
+import { UserService } from "../services/user.service";
+
+@Injectable()
+export class ConsentsResolver implements Resolve<any> {
+
+  constructor(private userService: UserService) { }
+
+  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>|Promise<any>|any {
+    let domainId = route.parent.parent.parent.paramMap.get('domainId');
+    let userId = route.parent.paramMap.get('userId');
+    let clientId = route.paramMap.get('clientId');
+    return this.userService.consents(domainId, userId, clientId).map(res => res.json());
+  }
+}
diff --git a/gravitee-am-ui/src/app/services/user.service.ts b/gravitee-am-ui/src/app/services/user.service.ts
index 0ac3644db6..bc4ddebef5 100644
--- a/gravitee-am-ui/src/app/services/user.service.ts
+++ b/gravitee-am-ui/src/app/services/user.service.ts
@@ -65,4 +65,15 @@ export class UserService {
     return this.http.get(this.usersURL + domainId + "/users?q=" + searchTerm + "&page=" + page + "&size=" + size);
   }
 
+  consents(domainId, userId, clientId) {
+    return this.http.get(this.usersURL + domainId + "/users/" + userId + "/consents" + (clientId ? "?clientId=" + clientId : ""));
+  }
+
+  revokeConsents(domainId, userId, clientId) {
+    return this.http.delete(this.usersURL + domainId + "/users/" + userId + "/consents" + (clientId ? "?clientId=" + clientId : ""));
+  }
+
+  revokeConsent(domainId, userId, consentId) {
+    return this.http.delete(this.usersURL + domainId + "/users/" + userId + "/consents/" + consentId);
+  }
 }
diff --git a/postman/graviteeam-collection.json b/postman/graviteeam-collection.json
index aa084db319..f24079cb77 100644
--- a/postman/graviteeam-collection.json
+++ b/postman/graviteeam-collection.json
@@ -1,6 +1,6 @@
 {
 	"info": {
-		"_postman_id": "0e42b2e3-9f9c-46d8-8a31-51e819a04c00",
+		"_postman_id": "c2e2552c-f861-4307-bd50-c570cb6d5701",
 		"name": "Gravitee.io AM",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
 	},
@@ -4505,7 +4505,7 @@
 							"name": "Case - With state parameter - Multiple clients",
 							"item": [
 								{
-									"name": "Initiate the flow -Client 1",
+									"name": "Initiate the flow - Client 2",
 									"event": [
 										{
 											"listen": "test",
@@ -4582,7 +4582,7 @@
 									"response": []
 								},
 								{
-									"name": "Redirect to login form - Client 1",
+									"name": "Redirect to login form - Client 2",
 									"event": [
 										{
 											"listen": "test",
@@ -4634,7 +4634,7 @@
 									"response": []
 								},
 								{
-									"name": "Post login form - Client 1",
+									"name": "Post login form - Client 2",
 									"event": [
 										{
 											"listen": "test",
@@ -4714,7 +4714,7 @@
 									"response": []
 								},
 								{
-									"name": "Follow redirection - Client 1",
+									"name": "Follow redirection - Client 2",
 									"event": [
 										{
 											"listen": "test",
@@ -4773,7 +4773,7 @@
 									"response": []
 								},
 								{
-									"name": "Initiate the flow -Client 2",
+									"name": "Initiate the flow - Client 1",
 									"event": [
 										{
 											"listen": "test",
@@ -4850,7 +4850,7 @@
 									"response": []
 								},
 								{
-									"name": "Redirect to confirm access page - Client 2",
+									"name": "Redirect to confirm access page - Client 1",
 									"event": [
 										{
 											"listen": "test",
@@ -4902,7 +4902,7 @@
 									"response": []
 								},
 								{
-									"name": "Post authorize form - Client 2",
+									"name": "Post authorize form - Client 1",
 									"event": [
 										{
 											"listen": "test",
@@ -4955,6 +4955,11 @@
 													"value": "true",
 													"type": "text"
 												},
+												{
+													"key": "scope.openid",
+													"value": "true",
+													"type": "text"
+												},
 												{
 													"key": "user_oauth_approval",
 													"value": "true",
@@ -4978,7 +4983,7 @@
 									"response": []
 								},
 								{
-									"name": "Follow redirection - Client 2",
+									"name": "Follow redirection - Client 1",
 									"event": [
 										{
 											"listen": "test",
@@ -5037,7 +5042,7 @@
 									"response": []
 								},
 								{
-									"name": "Logout user - Client 1",
+									"name": "Logout user",
 									"event": [
 										{
 											"listen": "test",
@@ -9637,6 +9642,655 @@
 								}
 							],
 							"_postman_isSubFolder": true
+						},
+						{
+							"name": "Case - revoke consent",
+							"item": [
+								{
+									"name": "Initiate the flow",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "fb56a849-b090-4e80-9c8c-eca9f4d72e85",
+												"exec": [
+													"pm.test(\"Status code is 302\", function () {",
+													"    pm.response.to.have.status(302);",
+													"});",
+													"",
+													"pm.test(\"Should be redirected\", function () {",
+													"    pm.response.to.be.redirection;",
+													"    pm.response.to.have.header('Location');",
+													"});",
+													"",
+													"pm.test(\"Should be a redirection to login page\", function() {",
+													"    var location = postman.getResponseHeader('Location');",
+													"    tests['Redirect to login page with client_id'] = location === pm.environment.get('gateway_url') + '/test/login?client_id=my-client2';",
+													"    ",
+													"    pm.environment.set('redirection', postman.getResponseHeader(\"Location\"));",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"exec": [
+													""
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
+											"raw": "{{gateway_url}}/test/oauth/authorize?response_type=token&client_id=my-client2&redirect_uri=http://localhost:4000/",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"test",
+												"oauth",
+												"authorize"
+											],
+											"query": [
+												{
+													"key": "response_type",
+													"value": "token"
+												},
+												{
+													"key": "client_id",
+													"value": "my-client2"
+												},
+												{
+													"key": "redirect_uri",
+													"value": "http://localhost:4000/"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Redirect to login form",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "67df9bf8-89a2-4c95-9728-097aad6d2be4",
+												"type": "text/javascript",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"Should be ok\", function () {",
+													"    pm.response.to.be.ok;",
+													"    ",
+													"    // Extract the XSRF token to send it with the next request.",
+													"    var responseHTML = cheerio(pm.response.text());",
+													"    var xsrfToken = responseHTML.find('[name=\"X-XSRF-TOKEN\"]').val();",
+													"    pm.environment.set('xsrf', xsrfToken);",
+													"});"
+												]
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"type": "text/javascript",
+												"exec": [
+													""
+												]
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
+											"raw": "{{redirection}}",
+											"host": [
+												"{{redirection}}"
+											]
+										},
+										"description": "The client does not have a redirect_uri define"
+									},
+									"response": []
+								},
+								{
+									"name": "Post login form",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "4afec9f2-a5ba-4647-9a78-6d74cb9c157c",
+												"exec": [
+													"pm.test(\"Status code is 302\", function () {",
+													"    pm.response.to.have.status(302);",
+													"});",
+													"",
+													"pm.test(\"Should be redirected\", function () {",
+													"    pm.response.to.be.redirection;",
+													"    pm.response.to.have.header('Location');",
+													"",
+													"    pm.environment.set('redirection', postman.getResponseHeader(\"Location\"));",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"exec": [
+													""
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/x-www-form-urlencoded"
+											}
+										],
+										"body": {
+											"mode": "urlencoded",
+											"urlencoded": [
+												{
+													"key": "X-XSRF-TOKEN",
+													"value": "{{xsrf}}",
+													"type": "text"
+												},
+												{
+													"key": "client_id",
+													"value": "my-client2",
+													"type": "text"
+												},
+												{
+													"key": "username",
+													"value": "user",
+													"type": "text"
+												},
+												{
+													"key": "password",
+													"value": "password",
+													"type": "text"
+												}
+											]
+										},
+										"url": {
+											"raw": "{{gateway_url}}/test/login",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"test",
+												"login"
+											]
+										},
+										"description": "The client does not have a redirect_uri define"
+									},
+									"response": []
+								},
+								{
+									"name": "Follow redirection",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "73f280ec-f0ac-46ec-9424-0cdecd34f1c8",
+												"exec": [
+													"pm.test(\"Status code is 302\", function () {",
+													"    pm.response.to.have.status(302);",
+													"});",
+													"",
+													"pm.test(\"Should be redirected\", function () {",
+													"    pm.response.to.be.redirection;",
+													"    pm.response.to.have.header('Location');",
+													"});",
+													"",
+													"pm.test(\"Should be a redirection to redirect_uri with implicit response\", function() {",
+													"    var location = postman.getResponseHeader('Location');",
+													"    tests['Redirect to redirect_uri'] = location.includes('http://localhost:4000/#');",
+													"    ",
+													"    tests['Contains an access_token param'] = location.includes('access_token=');",
+													"    tests['Contains a token_type param'] = location.includes('token_type=bearer');",
+													"    tests['Contains an expires_in param'] = location.includes('expires_in=');",
+													"    tests['Contains a scope param'] = location.includes('scope=scope1');",
+													"    ",
+													"    // get user id",
+													"    const paramsString = location.split('#')[1];",
+													"    const eachParamArray = paramsString.split('&');",
+													"    ",
+													"    let params = {};",
+													"    eachParamArray.forEach((param) => {",
+													"        const key = param.split('=')[0];",
+													"        const value = param.split('=')[1];",
+													"        Object.assign(params, {[key]: value});",
+													"    });",
+													"    ",
+													"    let jwt = jwt_decode(params['access_token']);",
+													"    pm.environment.set('consentUserId', jwt.sub);",
+													"    pm.environment.set('consentAccessToken', params['access_token']);",
+													"});",
+													"",
+													"function jwt_decode(jwt) {",
+													"    var parts = jwt.split('.'); // header, payload, signature",
+													"    return JSON.parse(atob(parts[1]));",
+													"}"
+												],
+												"type": "text/javascript"
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"exec": [
+													""
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
+											"raw": "{{redirection}}",
+											"host": [
+												"{{redirection}}"
+											]
+										},
+										"description": "The client does not have a redirect_uri define"
+									},
+									"response": []
+								},
+								{
+									"name": "Revoke user consent",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "7af1ea20-73eb-4d45-90d1-24e5d137eae9",
+												"exec": [
+													"pm.test(\"Status code is 204\", function () {",
+													"    pm.response.to.have.status(204);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "DELETE",
+										"header": [
+											{
+												"key": "Authorization",
+												"value": "Bearer {{consentAccessToken}}",
+												"type": "text"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
+											"raw": "{{gateway_url}}/test/users/{{consentUserId}}/consents",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"test",
+												"users",
+												"{{consentUserId}}",
+												"consents"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Initiate the flow (after consent revocation)",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "fb56a849-b090-4e80-9c8c-eca9f4d72e85",
+												"exec": [
+													"pm.test(\"Status code is 302\", function () {",
+													"    pm.response.to.have.status(302);",
+													"});",
+													"",
+													"pm.test(\"Should be redirected\", function () {",
+													"    pm.response.to.be.redirection;",
+													"    ",
+													"    pm.response.to.be.header('Content-Length', '0');",
+													"    pm.response.to.have.header('Location');",
+													"    pm.response.to.have.header('Set-Cookie');",
+													"});",
+													"",
+													"pm.test(\"Should be redirected to confirm_access page\", function() {",
+													"    var location = postman.getResponseHeader('Location');",
+													"    tests['Redirect to confirm_access page'] = location === pm.environment.get('gateway_url') + '/test/oauth/confirm_access';",
+													"    ",
+													"    pm.environment.set('redirection', postman.getResponseHeader(\"Location\"));",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"exec": [
+													""
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
+											"raw": "{{gateway_url}}/test/oauth/authorize?response_type=token&client_id=my-client2&redirect_uri=http://localhost:4000/",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"test",
+												"oauth",
+												"authorize"
+											],
+											"query": [
+												{
+													"key": "response_type",
+													"value": "token"
+												},
+												{
+													"key": "client_id",
+													"value": "my-client2"
+												},
+												{
+													"key": "redirect_uri",
+													"value": "http://localhost:4000/"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Redirect to confirm access page",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "3b80138d-68ff-4dc5-a6b3-ff08ab98e89a",
+												"type": "text/javascript",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"Should be ok\", function () {",
+													"    pm.response.to.be.ok;",
+													"    ",
+													"    // Extract the XSRF token to send it with the next request.",
+													"    var responseHTML = cheerio(pm.response.text());",
+													"    var xsrfToken = responseHTML.find('[name=\"X-XSRF-TOKEN\"]').val();",
+													"    pm.environment.set('xsrf', xsrfToken);",
+													"});"
+												]
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"type": "text/javascript",
+												"exec": [
+													""
+												]
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
+											"raw": "{{redirection}}",
+											"host": [
+												"{{redirection}}"
+											]
+										},
+										"description": "The client does not have a redirect_uri define"
+									},
+									"response": []
+								},
+								{
+									"name": "Post authorize form",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "dd53996a-9c23-4eaf-b618-1be0ca5fca63",
+												"exec": [
+													"pm.test(\"Status code is 302\", function () {",
+													"    pm.response.to.have.status(302);",
+													"});",
+													"",
+													"pm.test(\"Should be redirected\", function () {",
+													"    pm.response.to.be.redirection;",
+													"    pm.response.to.have.header('Location');",
+													"    ",
+													"    pm.environment.set('redirection', postman.getResponseHeader(\"Location\"));",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"exec": [
+													""
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/x-www-form-urlencoded"
+											}
+										],
+										"body": {
+											"mode": "urlencoded",
+											"urlencoded": [
+												{
+													"key": "X-XSRF-TOKEN",
+													"value": "{{xsrf}}",
+													"type": "text"
+												},
+												{
+													"key": "scope.scope1",
+													"value": "true",
+													"type": "text"
+												},
+												{
+													"key": "user_oauth_approval",
+													"value": "true",
+													"type": "text"
+												}
+											]
+										},
+										"url": {
+											"raw": "{{gateway_url}}/test/oauth/authorize",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"test",
+												"oauth",
+												"authorize"
+											]
+										},
+										"description": "The client does not have a redirect_uri define"
+									},
+									"response": []
+								},
+								{
+									"name": "Follow redirection",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "6ed51b26-0e51-4c2e-87c1-8dd8433435f2",
+												"exec": [
+													"pm.test(\"Status code is 302\", function () {",
+													"    pm.response.to.have.status(302);",
+													"});",
+													"",
+													"pm.test(\"Should be redirected\", function () {",
+													"    pm.response.to.be.redirection;",
+													"    pm.response.to.have.header('Location');",
+													"});",
+													"",
+													"pm.test(\"Should be a redirection to redirect_uri with implicit response\", function() {",
+													"    var location = postman.getResponseHeader('Location');",
+													"    tests['Redirect to redirect_uri'] = location.includes('http://localhost:4000/#');",
+													"    ",
+													"    tests['Contains an access_token param'] = location.includes('access_token=');",
+													"    tests['Contains a token_type param'] = location.includes('token_type=bearer');",
+													"    tests['Contains an expires_in param'] = location.includes('expires_in=');",
+													"    tests['Contains a scope param'] = location.includes('scope=scope1');",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"exec": [
+													""
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
+											"raw": "{{redirection}}",
+											"host": [
+												"{{redirection}}"
+											]
+										},
+										"description": "The client does not have a redirect_uri define"
+									},
+									"response": []
+								},
+								{
+									"name": "Logout user",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "ba6b6fb1-32ac-46d5-8e2d-f97961029a05",
+												"exec": [
+													"pm.test(\"Status code is 302\", function () {",
+													"    pm.response.to.have.status(302);",
+													"});",
+													"",
+													"pm.test(\"Should be redirected\", function () {",
+													"    pm.response.to.be.redirection;",
+													"    pm.response.to.have.header('Location');",
+													"    ",
+													"    pm.environment.set('redirection', postman.getResponseHeader(\"Location\"));",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"exec": [
+													""
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
+											"raw": "{{gateway_url}}/test/logout",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"test",
+												"logout"
+											]
+										},
+										"description": "The client does not have a redirect_uri define"
+									},
+									"response": []
+								}
+							],
+							"_postman_isSubFolder": true
 						}
 					],
 					"_postman_isSubFolder": true


Test patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/oauth2/approval/ApprovalServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/oauth2/approval/ApprovalServiceTest.java
index e180ca2f20..d9043c40b4 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/oauth2/approval/ApprovalServiceTest.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/oauth2/approval/ApprovalServiceTest.java
@@ -23,6 +23,7 @@ import io.gravitee.am.gateway.handler.oauth2.scope.ScopeManager;
 import io.gravitee.am.gateway.handler.oauth2.utils.OAuth2Constants;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.User;
 import io.gravitee.am.model.oauth2.ScopeApproval;
 import io.gravitee.am.repository.oauth2.api.ScopeApprovalRepository;
 import io.reactivex.Maybe;
@@ -71,13 +72,16 @@ public class ApprovalServiceTest {
         client.setClientId(clientId);
         client.setAutoApproveScopes(Collections.singletonList(autoApproveScope));
 
+        User user = new User();
+        user.setId(userId);
+
         AuthorizationRequest authorizationRequest = new AuthorizationRequest();
         authorizationRequest.setClientId(clientId);
         authorizationRequest.setScopes(Collections.singleton(autoApproveScope));
 
         when(clientSyncService.findByClientId(clientId)).thenReturn(Maybe.just(client));
 
-        TestObserver<AuthorizationRequest> testObserver = approvalService.checkApproval(authorizationRequest, client, userId).test();
+        TestObserver<AuthorizationRequest> testObserver = approvalService.checkApproval(authorizationRequest, client, user).test();
         testObserver.awaitTerminalEvent();
 
         testObserver.assertComplete();
@@ -94,6 +98,9 @@ public class ApprovalServiceTest {
         client.setClientId(clientId);
         client.setAutoApproveScopes(null);
 
+        User user = new User();
+        user.setId(userId);
+
         AuthorizationRequest authorizationRequest = new AuthorizationRequest();
         authorizationRequest.setClientId(clientId);
         authorizationRequest.setScopes(Collections.singleton(autoApproveScope));
@@ -101,7 +108,7 @@ public class ApprovalServiceTest {
         when(clientSyncService.findByClientId(clientId)).thenReturn(Maybe.just(client));
         when(scopeApprovalRepository.findByDomainAndUserAndClient(anyString(), anyString(), anyString())).thenReturn(Single.just(Collections.emptySet()));
 
-        approvalService.checkApproval(authorizationRequest, client, userId).test().assertError(AccessDeniedException.class);
+        approvalService.checkApproval(authorizationRequest, client, user).test().assertError(AccessDeniedException.class);
     }
 
     @Test
@@ -114,6 +121,9 @@ public class ApprovalServiceTest {
         client.setClientId(clientId);
         client.setAutoApproveScopes(null);
 
+        User user = new User();
+        user.setId(userId);
+
         ScopeApproval userScopeApproval = new ScopeApproval();
         userScopeApproval.setClientId(clientId);
         userScopeApproval.setUserId(userId);
@@ -130,7 +140,7 @@ public class ApprovalServiceTest {
         when(clientSyncService.findByClientId(clientId)).thenReturn(Maybe.just(client));
         when(scopeApprovalRepository.findByDomainAndUserAndClient(domainId, userId, clientId)).thenReturn(Single.just(Collections.singleton(userScopeApproval)));
 
-        TestObserver<AuthorizationRequest> testObserver = approvalService.checkApproval(authorizationRequest, client, userId).test();
+        TestObserver<AuthorizationRequest> testObserver = approvalService.checkApproval(authorizationRequest, client, user).test();
         testObserver.awaitTerminalEvent();
 
         testObserver.assertComplete();
@@ -148,6 +158,9 @@ public class ApprovalServiceTest {
         client.setClientId(clientId);
         client.setAutoApproveScopes(null);
 
+        User user = new User();
+        user.setId(userId);
+
         ScopeApproval userScopeApproval = new ScopeApproval();
         userScopeApproval.setClientId(clientId);
         userScopeApproval.setUserId(userId);
@@ -164,7 +177,7 @@ public class ApprovalServiceTest {
         when(clientSyncService.findByClientId(clientId)).thenReturn(Maybe.just(client));
         when(scopeApprovalRepository.findByDomainAndUserAndClient(domainId, userId, clientId)).thenReturn(Single.just(Collections.singleton(userScopeApproval)));
 
-        approvalService.checkApproval(authorizationRequest, client, userId).test().assertError(AccessDeniedException.class);
+        approvalService.checkApproval(authorizationRequest, client, user).test().assertError(AccessDeniedException.class);
     }
 
     @Test
@@ -177,6 +190,9 @@ public class ApprovalServiceTest {
         client.setClientId(clientId);
         client.setAutoApproveScopes(Collections.singletonList(readScope));
 
+        User user = new User();
+        user.setId(userId);
+
         AuthorizationRequest authorizationRequest = new AuthorizationRequest();
         authorizationRequest.setClientId(clientId);
         authorizationRequest.setDeniedScopes(new HashSet<>(Arrays.asList(readScope, writeScope)));
@@ -188,7 +204,7 @@ public class ApprovalServiceTest {
 
         when(scopeApprovalRepository.upsert(any())).thenReturn(Single.just(new ScopeApproval()));
 
-        TestObserver<AuthorizationRequest> testObserver = approvalService.saveApproval(authorizationRequest, client, userId).test();
+        TestObserver<AuthorizationRequest> testObserver = approvalService.saveApproval(authorizationRequest, client, user).test();
         testObserver.awaitTerminalEvent();
 
         testObserver.assertComplete();
@@ -208,6 +224,9 @@ public class ApprovalServiceTest {
         client.setClientId(clientId);
         client.setAutoApproveScopes(Collections.singletonList(readScope));
 
+        User user = new User();
+        user.setId(userId);
+
         AuthorizationRequest authorizationRequest = new AuthorizationRequest();
         authorizationRequest.setClientId(clientId);
         authorizationRequest.setDeniedScopes(new HashSet<>(Arrays.asList(readScope, writeScope)));
@@ -219,7 +238,7 @@ public class ApprovalServiceTest {
 
         when(scopeApprovalRepository.upsert(any())).thenReturn(Single.just(new ScopeApproval()));
 
-        TestObserver<AuthorizationRequest> testObserver = approvalService.saveApproval(authorizationRequest, client, userId).test();
+        TestObserver<AuthorizationRequest> testObserver = approvalService.saveApproval(authorizationRequest, client, user).test();
         testObserver.awaitTerminalEvent();
 
         testObserver.assertComplete();
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/user/UserServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/user/UserServiceTest.java
new file mode 100644
index 0000000000..b166df89fc
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/user/UserServiceTest.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.user;
+
+import io.gravitee.am.gateway.handler.user.impl.UserServiceImpl;
+import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.oauth2.ScopeApproval;
+import io.gravitee.am.service.ScopeApprovalService;
+import io.gravitee.am.service.exception.ScopeApprovalNotFoundException;
+import io.reactivex.Completable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.reactivex.observers.TestObserver;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.Collections;
+import java.util.Set;
+
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class UserServiceTest {
+
+    @InjectMocks
+    private UserService userService = new UserServiceImpl();
+
+    @Mock
+    private Domain domain;
+
+    @Mock
+    private ScopeApprovalService scopeApprovalService;
+
+    @Test
+    public void shouldFindUserConsents() {
+        final String userId = "userId";
+
+        final ScopeApproval scopeApproval = new ScopeApproval();
+        scopeApproval.setId("consentId");
+
+        when(scopeApprovalService.findByDomainAndUser(anyString(), eq(userId))).thenReturn(Single.just(Collections.singleton(scopeApproval)));
+
+        TestObserver<Set<ScopeApproval>> testObserver = userService.consents(userId).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValue(scopeApprovals -> scopeApprovals.iterator().next().getId().equals("consentId"));
+    }
+
+    @Test
+    public void shouldFindUserConsent() {
+        final ScopeApproval scopeApproval = new ScopeApproval();
+        scopeApproval.setId("consentId");
+
+        when(scopeApprovalService.findById(anyString())).thenReturn(Maybe.just(scopeApproval));
+
+        TestObserver<ScopeApproval> testObserver = userService.consent("consentId").test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValue(scopeApproval1 -> scopeApproval1.getId().equals("consentId"));
+    }
+
+    @Test
+    public void shouldNotFindUserConsent_consentNotFound() {
+        final ScopeApproval scopeApproval = new ScopeApproval();
+        scopeApproval.setId("consentId");
+
+        when(scopeApprovalService.findById(anyString())).thenReturn(Maybe.empty());
+
+        TestObserver<ScopeApproval> testObserver = userService.consent("consentId").test();
+
+        testObserver.assertNotComplete();
+        testObserver.assertError(ScopeApprovalNotFoundException.class);
+    }
+
+    @Test
+    public void shouldRevokeConsents() {
+        final String userId = "userId";
+
+        final ScopeApproval scopeApproval = new ScopeApproval();
+        scopeApproval.setId("consentId");
+
+        when(scopeApprovalService.revoke(anyString(), eq(userId))).thenReturn(Completable.complete());
+
+        TestObserver testObserver = userService.revokeConsents(userId).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+    }
+
+    @Test
+    public void shouldRevokeConsent() {
+        when(scopeApprovalService.revoke(anyString())).thenReturn(Completable.complete());
+
+        TestObserver testObserver = userService.revokeConsent("consentId").test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentEndpointHandlerTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentEndpointHandlerTest.java
new file mode 100644
index 0000000000..8b1ca69d31
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentEndpointHandlerTest.java
@@ -0,0 +1,182 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.vertx.handler.users.endpoint.consents;
+
+import io.gravitee.am.common.jwt.JWT;
+import io.gravitee.am.gateway.handler.jwt.JwtService;
+import io.gravitee.am.gateway.handler.oauth2.client.ClientSyncService;
+import io.gravitee.am.gateway.handler.oauth2.token.Token;
+import io.gravitee.am.gateway.handler.oauth2.token.TokenService;
+import io.gravitee.am.gateway.handler.oauth2.token.impl.AccessToken;
+import io.gravitee.am.gateway.handler.user.UserService;
+import io.gravitee.am.gateway.handler.vertx.RxWebTestBase;
+import io.gravitee.am.gateway.handler.vertx.handler.users.handler.AuthTokenParseHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.users.handler.ErrorHandler;
+import io.gravitee.am.model.Client;
+import io.gravitee.am.model.oauth2.ScopeApproval;
+import io.gravitee.am.service.exception.ScopeApprovalNotFoundException;
+import io.gravitee.common.http.HttpStatusCode;
+import io.reactivex.Completable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.vertx.core.http.HttpHeaders;
+import io.vertx.core.http.HttpMethod;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class UserConsentEndpointHandlerTest extends RxWebTestBase {
+
+    @Mock
+    private UserService userService;
+
+    @Mock
+    private JwtService jwtService;
+
+    @Mock
+    private ClientSyncService clientService;
+
+    @Mock
+    private TokenService tokenService;
+
+    @InjectMocks
+    private UserConsentEndpointHandler userConsentEndpointHandler = new UserConsentEndpointHandler(userService);
+
+    @InjectMocks
+    private AuthTokenParseHandler authTokenParseHandler = AuthTokenParseHandler.create(jwtService, tokenService, clientService, "consent_admin");
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Test
+    public void shouldNotGetConsent_no_token() throws Exception {
+        router.route("/users/:userId/consents/:consentId")
+                .handler(authTokenParseHandler)
+                .handler(userConsentEndpointHandler::get)
+                .failureHandler(new ErrorHandler());
+
+        testRequest(
+                HttpMethod.GET, "/users/user-id/consents/consent-id",
+                HttpStatusCode.UNAUTHORIZED_401, "Unauthorized");
+
+    }
+
+    @Test
+    public void shouldNotGetConsent_invalid_token() throws Exception {
+        Token token = new AccessToken("uuid");
+        token.setExpiresIn(10000);
+        token.setScope("read");
+
+        when(jwtService.decode(anyString())).thenReturn(Single.just(new JWT()));
+        when(clientService.findByClientId(anyString())).thenReturn(Maybe.just(new Client()));
+        when(tokenService.getAccessToken(anyString(), any())).thenReturn(Maybe.just(token));
+
+        router.route("/users/:userId/consents/:consentId")
+                .handler(authTokenParseHandler)
+                .handler(userConsentEndpointHandler::get)
+                .failureHandler(new ErrorHandler());
+
+        testRequest(
+                HttpMethod.GET, "/users/user-id/consents/consent-id",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer token"),
+                401,
+                "Unauthorized", null);
+    }
+
+    @Test
+    public void shouldNotGetConsent_notFound() throws Exception {
+        Token token = new AccessToken("uuid");
+        token.setExpiresIn(10000);
+        token.setScope("consent_admin");
+
+        when(jwtService.decode(anyString())).thenReturn(Single.just(new JWT()));
+        when(clientService.findByClientId(anyString())).thenReturn(Maybe.just(new Client()));
+        when(tokenService.getAccessToken(anyString(), any())).thenReturn(Maybe.just(token));
+        when(userService.consent(anyString())).thenReturn(Maybe.error(new ScopeApprovalNotFoundException("consentId")));
+
+        router.route("/users/:userId/consents/:consentId")
+                .handler(authTokenParseHandler)
+                .handler(userConsentEndpointHandler::get)
+                .failureHandler(new ErrorHandler());
+
+        testRequest(
+                HttpMethod.GET, "/users/user-id/consents/consent-id",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer token"),
+                404,
+                "Not Found", null);
+
+    }
+
+    @Test
+    public void shouldGetConsent() throws Exception {
+        Token token = new AccessToken("uuid");
+        token.setExpiresIn(10000);
+        token.setScope("consent_admin");
+
+        when(jwtService.decode(anyString())).thenReturn(Single.just(new JWT()));
+        when(clientService.findByClientId(anyString())).thenReturn(Maybe.just(new Client()));
+        when(tokenService.getAccessToken(anyString(), any())).thenReturn(Maybe.just(token));
+        when(userService.consent(anyString())).thenReturn(Maybe.just(new ScopeApproval()));
+
+        router.route("/users/:userId/consents/:consentId")
+                .handler(authTokenParseHandler)
+                .handler(userConsentEndpointHandler::get)
+                .failureHandler(new ErrorHandler());
+
+        testRequest(
+                HttpMethod.GET, "/users/user-id/consents/consent-id",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer token"),
+                200,
+                "OK", null);
+
+    }
+
+    @Test
+    public void shouldRevokeConsent() throws Exception {
+        Token token = new AccessToken("uuid");
+        token.setExpiresIn(10000);
+        token.setScope("consent_admin");
+
+        when(jwtService.decode(anyString())).thenReturn(Single.just(new JWT()));
+        when(clientService.findByClientId(anyString())).thenReturn(Maybe.just(new Client()));
+        when(tokenService.getAccessToken(anyString(), any())).thenReturn(Maybe.just(token));
+        when(userService.revokeConsent(anyString())).thenReturn(Completable.complete());
+
+        router.route("/users/:userId/consents/:consentId")
+                .handler(authTokenParseHandler)
+                .handler(userConsentEndpointHandler::revoke)
+                .failureHandler(new ErrorHandler());
+
+        testRequest(
+                HttpMethod.DELETE, "/users/user-id/consents/consent-id",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer token"),
+                204,
+                "No Content", null);
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentsEndpointHandlerTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentsEndpointHandlerTest.java
new file mode 100644
index 0000000000..6746fd4323
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/users/endpoint/consents/UserConsentsEndpointHandlerTest.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.vertx.handler.users.endpoint.consents;
+
+import io.gravitee.am.common.jwt.JWT;
+import io.gravitee.am.gateway.handler.jwt.JwtService;
+import io.gravitee.am.gateway.handler.oauth2.client.ClientSyncService;
+import io.gravitee.am.gateway.handler.oauth2.token.Token;
+import io.gravitee.am.gateway.handler.oauth2.token.TokenService;
+import io.gravitee.am.gateway.handler.oauth2.token.impl.AccessToken;
+import io.gravitee.am.gateway.handler.user.UserService;
+import io.gravitee.am.gateway.handler.vertx.RxWebTestBase;
+import io.gravitee.am.gateway.handler.vertx.handler.users.handler.AuthTokenParseHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.users.handler.ErrorHandler;
+import io.gravitee.am.model.Client;
+import io.gravitee.am.model.oauth2.ScopeApproval;
+import io.gravitee.common.http.HttpStatusCode;
+import io.reactivex.Completable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.vertx.core.http.HttpHeaders;
+import io.vertx.core.http.HttpMethod;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.Collections;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class UserConsentsEndpointHandlerTest extends RxWebTestBase {
+
+    @Mock
+    private UserService userService;
+
+    @Mock
+    private JwtService jwtService;
+
+    @Mock
+    private ClientSyncService clientService;
+
+    @Mock
+    private TokenService tokenService;
+
+    @InjectMocks
+    private UserConsentsEndpointHandler userConsentsEndpointHandler = new UserConsentsEndpointHandler(userService);
+
+    @InjectMocks
+    private AuthTokenParseHandler authTokenParseHandler = AuthTokenParseHandler.create(jwtService, tokenService, clientService, "consent_admin");
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Test
+    public void shouldNotListConsents_no_token() throws Exception {
+        router.route("/users/:userId/consents")
+                .handler(authTokenParseHandler)
+                .handler(userConsentsEndpointHandler::list)
+                .failureHandler(new ErrorHandler());
+
+        testRequest(
+                HttpMethod.GET, "/users/user-id/consents",
+                HttpStatusCode.UNAUTHORIZED_401, "Unauthorized");
+
+    }
+
+    @Test
+    public void shouldNotListConsents_invalid_token() throws Exception {
+        Token token = new AccessToken("uuid");
+        token.setExpiresIn(10000);
+        token.setScope("read");
+
+        when(jwtService.decode(anyString())).thenReturn(Single.just(new JWT()));
+        when(clientService.findByClientId(anyString())).thenReturn(Maybe.just(new Client()));
+        when(tokenService.getAccessToken(anyString(), any())).thenReturn(Maybe.just(token));
+
+        router.route("/users/:userId/consents")
+                .handler(authTokenParseHandler)
+                .handler(userConsentsEndpointHandler::list)
+                .failureHandler(new ErrorHandler());
+
+        testRequest(
+                HttpMethod.GET, "/users/user-id/consents",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer token"),
+                401,
+                "Unauthorized", null);
+    }
+
+    @Test
+    public void shouldListConsents() throws Exception {
+        Token token = new AccessToken("uuid");
+        token.setExpiresIn(10000);
+        token.setScope("consent_admin");
+
+        when(jwtService.decode(anyString())).thenReturn(Single.just(new JWT()));
+        when(clientService.findByClientId(anyString())).thenReturn(Maybe.just(new Client()));
+        when(tokenService.getAccessToken(anyString(), any())).thenReturn(Maybe.just(token));
+        when(userService.consents(anyString())).thenReturn(Single.just(Collections.singleton(new ScopeApproval())));
+
+        router.route("/users/:userId/consents")
+                .handler(authTokenParseHandler)
+                .handler(userConsentsEndpointHandler::list)
+                .failureHandler(new ErrorHandler());
+
+        testRequest(
+                HttpMethod.GET, "/users/user-id/consents",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer token"),
+                200,
+                "OK", null);
+
+    }
+
+    @Test
+    public void shouldRevokeConsents() throws Exception {
+        Token token = new AccessToken("uuid");
+        token.setExpiresIn(10000);
+        token.setScope("consent_admin");
+
+        when(jwtService.decode(anyString())).thenReturn(Single.just(new JWT()));
+        when(clientService.findByClientId(anyString())).thenReturn(Maybe.just(new Client()));
+        when(tokenService.getAccessToken(anyString(), any())).thenReturn(Maybe.just(token));
+        when(userService.revokeConsents(anyString())).thenReturn(Completable.complete());
+
+        router.route("/users/:userId/consents")
+                .handler(authTokenParseHandler)
+                .handler(userConsentsEndpointHandler::revoke)
+                .failureHandler(new ErrorHandler());
+
+        testRequest(
+                HttpMethod.DELETE, "/users/user-id/consents",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer token"),
+                204,
+                "No Content", null);
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/JerseySpringTest.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/JerseySpringTest.java
index 7347980a2a..21b738c807 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/JerseySpringTest.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/JerseySpringTest.java
@@ -30,6 +30,7 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableMBeanExport;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.test.context.support.AnnotationConfigContextLoader;
@@ -101,6 +102,9 @@ public abstract class JerseySpringTest {
     @Autowired
     protected PasswordValidator passwordValidator;
 
+    @Autowired
+    protected ScopeApprovalService scopeApprovalService;
+
     @Configuration
     @ComponentScan("io.gravitee.am.management.handlers.management.api.resources.enhancer")
     static class ContextConfiguration {
@@ -198,6 +202,11 @@ public abstract class JerseySpringTest {
         public PasswordValidator passwordValidator() {
             return mock(PasswordValidator.class);
         }
+
+        @Bean
+        public ScopeApprovalService scopeApprovalService() {
+            return mock(ScopeApprovalService.class);
+        }
     }
 
     private JerseyTest _jerseyTest;
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentResourceTest.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentResourceTest.java
new file mode 100644
index 0000000000..6b768c1ea5
--- /dev/null
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentResourceTest.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.management.handlers.management.api.resources;
+
+import io.gravitee.am.management.handlers.management.api.JerseySpringTest;
+import io.gravitee.am.model.Client;
+import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.User;
+import io.gravitee.am.model.oauth2.Scope;
+import io.gravitee.am.model.oauth2.ScopeApproval;
+import io.gravitee.am.service.exception.TechnicalManagementException;
+import io.gravitee.common.http.HttpStatusCode;
+import io.reactivex.Completable;
+import io.reactivex.Maybe;
+import org.junit.Test;
+
+import javax.ws.rs.core.Response;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class UserConsentResourceTest extends JerseySpringTest {
+
+    @Test
+    public void shouldGetUserConsent() {
+        final String domainId = "domain-1";
+        final Domain mockDomain = new Domain();
+        mockDomain.setId(domainId);
+
+        final User mockUser = new User();
+        mockUser.setId("user-id-1");
+
+        final Client mockClient = new Client();
+        mockClient.setId("client-id-1");
+
+        final Scope mockScope = new Scope();
+        mockScope.setId("scope-id-1");
+        mockScope.setKey("scope");
+
+        final ScopeApproval scopeApproval = new ScopeApproval();
+        scopeApproval.setId("consent-id");
+        scopeApproval.setClientId("clientId");
+        scopeApproval.setScope("scope");
+        scopeApproval.setDomain(domainId);
+
+
+        doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
+        doReturn(Maybe.just(mockClient)).when(clientService).findByDomainAndClientId(domainId, scopeApproval.getClientId());
+        doReturn(Maybe.just(mockScope)).when(scopeService).findByDomainAndKey(domainId, scopeApproval.getScope());
+        doReturn(Maybe.just(scopeApproval)).when(scopeApprovalService).findById(scopeApproval.getId());
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("users")
+                .path(mockUser.getId())
+                .path("consents")
+                .path(scopeApproval.getId())
+                .request()
+                .get();
+
+        assertEquals(HttpStatusCode.OK_200, response.getStatus());
+    }
+
+    @Test
+    public void shouldGetUserConsent_technicalManagementException() {
+        final String domainId = "domain-1";
+        doReturn(Maybe.error(new TechnicalManagementException("error occurs"))).when(domainService).findById(domainId);
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("users")
+                .path("user1")
+                .path("consents")
+                .path("consent1")
+                .request().get();
+        assertEquals(HttpStatusCode.INTERNAL_SERVER_ERROR_500, response.getStatus());
+    }
+
+    @Test
+    public void shouldRevokeUserConsent() {
+        final String domainId = "domain-1";
+        final Domain mockDomain = new Domain();
+        mockDomain.setId(domainId);
+
+        final User mockUser = new User();
+        mockUser.setId("user-id-1");
+
+        doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
+        doReturn(Completable.complete()).when(scopeApprovalService).revoke("consent1");
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("users")
+                .path(mockUser.getId())
+                .path("consents")
+                .path("consent1")
+                .request()
+                .delete();
+
+        assertEquals(HttpStatusCode.NO_CONTENT_204, response.getStatus());
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentsResourceTest.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentsResourceTest.java
new file mode 100644
index 0000000000..ea0b44f90e
--- /dev/null
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UserConsentsResourceTest.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.management.handlers.management.api.resources;
+
+import io.gravitee.am.management.handlers.management.api.JerseySpringTest;
+import io.gravitee.am.model.Client;
+import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.User;
+import io.gravitee.am.model.oauth2.Scope;
+import io.gravitee.am.model.oauth2.ScopeApproval;
+import io.gravitee.am.service.exception.TechnicalManagementException;
+import io.gravitee.common.http.HttpStatusCode;
+import io.reactivex.Completable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import org.junit.Test;
+
+import javax.ws.rs.core.Response;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class UserConsentsResourceTest extends JerseySpringTest {
+
+    @Test
+    public void shouldGetUserConsents() {
+        final String domainId = "domain-1";
+        final Domain mockDomain = new Domain();
+        mockDomain.setId(domainId);
+
+        final User mockUser = new User();
+        mockUser.setId("user-id-1");
+
+        final Client mockClient = new Client();
+        mockClient.setId("client-id-1");
+
+        final Scope mockScope = new Scope();
+        mockScope.setId("scope-id-1");
+        mockScope.setKey("scope");
+
+        final ScopeApproval scopeApproval = new ScopeApproval();
+        scopeApproval.setClientId("clientId");
+        scopeApproval.setScope("scope");
+        scopeApproval.setDomain(domainId);
+
+
+        doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
+        doReturn(Maybe.just(mockClient)).when(clientService).findByDomainAndClientId(domainId, scopeApproval.getClientId());
+        doReturn(Maybe.just(mockScope)).when(scopeService).findByDomainAndKey(domainId, scopeApproval.getScope());
+        doReturn(Single.just(Collections.singleton(scopeApproval))).when(scopeApprovalService).findByDomainAndUser(domainId, mockUser.getId());
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("users")
+                .path(mockUser.getId())
+                .path("consents")
+                .request()
+                .get();
+
+        assertEquals(HttpStatusCode.OK_200, response.getStatus());
+    }
+
+    @Test
+    public void shouldGetUserConsents_technicalManagementException() {
+        final String domainId = "domain-1";
+        doReturn(Maybe.error(new TechnicalManagementException("error occurs"))).when(domainService).findById(domainId);
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("users")
+                .path("user1")
+                .path("consents")
+                .request().get();
+        assertEquals(HttpStatusCode.INTERNAL_SERVER_ERROR_500, response.getStatus());
+    }
+
+    @Test
+    public void shouldRevokeUserConsents() {
+        final String domainId = "domain-1";
+        final Domain mockDomain = new Domain();
+        mockDomain.setId(domainId);
+
+        final User mockUser = new User();
+        mockUser.setId("user-id-1");
+
+        doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
+        doReturn(Completable.complete()).when(scopeApprovalService).revoke(domainId, mockUser.getId());
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("users")
+                .path(mockUser.getId())
+                .path("consents")
+                .request()
+                .delete();
+
+        assertEquals(HttpStatusCode.NO_CONTENT_204, response.getStatus());
+    }
+
+}
diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/ScopeApprovalServiceTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/ScopeApprovalServiceTest.java
new file mode 100644
index 0000000000..18667dc346
--- /dev/null
+++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/ScopeApprovalServiceTest.java
@@ -0,0 +1,150 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.service;
+
+import io.gravitee.am.model.oauth2.ScopeApproval;
+import io.gravitee.am.repository.exceptions.TechnicalException;
+import io.gravitee.am.repository.oauth2.api.ScopeApprovalRepository;
+import io.gravitee.am.service.exception.TechnicalManagementException;
+import io.gravitee.am.service.impl.ScopeApprovalServiceImpl;
+import io.reactivex.Completable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.reactivex.observers.TestObserver;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.Collections;
+import java.util.Set;
+
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ScopeApprovalServiceTest {
+
+    @InjectMocks
+    private ScopeApprovalService scopeApprovalService = new ScopeApprovalServiceImpl();
+
+    @Mock
+    private ScopeApprovalRepository scopeApprovalRepository;
+
+    private final static String DOMAIN = "domain1";
+
+    @Test
+    public void shouldFindById() {
+        when(scopeApprovalRepository.findById("my-consent")).thenReturn(Maybe.just(new ScopeApproval()));
+        TestObserver testObserver = scopeApprovalService.findById("my-consent").test();
+
+        testObserver.awaitTerminalEvent();
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValueCount(1);
+    }
+
+    @Test
+    public void shouldFindById_notExistingScopeApproval() {
+        when(scopeApprovalRepository.findById("my-consent")).thenReturn(Maybe.empty());
+        TestObserver testObserver = scopeApprovalService.findById("my-consent").test();
+        testObserver.awaitTerminalEvent();
+
+        testObserver.assertNoValues();
+    }
+
+    @Test
+    public void shouldFindById_technicalException() {
+        when(scopeApprovalRepository.findById("my-consent")).thenReturn(Maybe.error(TechnicalException::new));
+        TestObserver testObserver = new TestObserver();
+        scopeApprovalService.findById("my-consent").subscribe(testObserver);
+
+        testObserver.assertError(TechnicalManagementException.class);
+        testObserver.assertNotComplete();
+    }
+
+    @Test
+    public void shouldFindByDomainAndUser() {
+        when(scopeApprovalRepository.findByDomainAndUser(DOMAIN, "userId")).thenReturn(Single.just(Collections.singleton(new ScopeApproval())));
+        TestObserver<Set<ScopeApproval>> testObserver = scopeApprovalService.findByDomainAndUser(DOMAIN, "userId").test();
+        testObserver.awaitTerminalEvent();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValue(scopeApprovals -> scopeApprovals.size() == 1);
+    }
+
+    @Test
+    public void shouldFindByDomainAndUser_technicalException() {
+        when(scopeApprovalRepository.findByDomainAndUser(DOMAIN, "userId")).thenReturn(Single.error(TechnicalException::new));
+
+        TestObserver testObserver = new TestObserver<>();
+        scopeApprovalService.findByDomainAndUser(DOMAIN, "userId").subscribe(testObserver);
+
+        testObserver.assertError(TechnicalManagementException.class);
+        testObserver.assertNotComplete();
+    }
+
+    @Test
+    public void shouldFindByDomainAndUserAndClient() {
+        when(scopeApprovalRepository.findByDomainAndUserAndClient(DOMAIN, "userId", "clientId")).thenReturn(Single.just(Collections.singleton(new ScopeApproval())));
+        TestObserver<Set<ScopeApproval>> testObserver = scopeApprovalService.findByDomainAndUserAndClient(DOMAIN, "userId", "clientId").test();
+        testObserver.awaitTerminalEvent();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValue(scopeApprovals -> scopeApprovals.size() == 1);
+    }
+
+    @Test
+    public void shouldFindByDomainAndUserAndClient_technicalException() {
+        when(scopeApprovalRepository.findByDomainAndUserAndClient(DOMAIN, "userId", "clientId")).thenReturn(Single.error(TechnicalException::new));
+
+        TestObserver testObserver = new TestObserver<>();
+        scopeApprovalService.findByDomainAndUserAndClient(DOMAIN, "userId", "clientId").subscribe(testObserver);
+
+        testObserver.assertError(TechnicalManagementException.class);
+        testObserver.assertNotComplete();
+    }
+
+    @Test
+    public void shouldDelete_technicalException() {
+        when(scopeApprovalRepository.delete(anyString())).thenReturn(Completable.error(TechnicalException::new));
+
+        TestObserver testObserver = scopeApprovalService.revoke("my-consent").test();
+
+        testObserver.assertError(TechnicalManagementException.class);
+        testObserver.assertNotComplete();
+    }
+
+    @Test
+    public void shouldDelete() {
+        when(scopeApprovalRepository.delete("my-consent")).thenReturn(Completable.complete());
+
+        TestObserver testObserver = scopeApprovalService.revoke( "my-consent").test();
+        testObserver.awaitTerminalEvent();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+
+        verify(scopeApprovalRepository, times(1)).delete("my-consent");
+    }
+}
diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/ScopeServiceTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/ScopeServiceTest.java
index 6eace574e1..2a22894e4f 100644
--- a/gravitee-am-service/src/test/java/io/gravitee/am/service/ScopeServiceTest.java
+++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/ScopeServiceTest.java
@@ -257,7 +257,7 @@ public class ScopeServiceTest {
         when(clientService.findByDomain(DOMAIN)).thenReturn(Single.just(Collections.emptySet()));
         when(scopeRepository.findById("my-scope")).thenReturn(Maybe.just(scope));
         when(scopeRepository.delete("my-scope")).thenReturn(Completable.complete());
-        when(scopeApprovalRepository.delete(scope.getDomain(), scope.getKey())).thenReturn(Completable.complete());
+        when(scopeApprovalRepository.deleteByDomainAndScopeKey(scope.getDomain(), scope.getKey())).thenReturn(Completable.complete());
         when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
 
         TestObserver testObserver = scopeService.delete("my-scope", false).test();
@@ -292,7 +292,7 @@ public class ScopeServiceTest {
         when(clientService.patch(anyString(),anyString(),any(PatchClient.class))).thenReturn(Single.just(new Client()));
         when(scopeRepository.findById("my-scope")).thenReturn(Maybe.just(scope));
         when(scopeRepository.delete("my-scope")).thenReturn(Completable.complete());
-        when(scopeApprovalRepository.delete(scope.getDomain(), scope.getKey())).thenReturn(Completable.complete());
+        when(scopeApprovalRepository.deleteByDomainAndScopeKey(scope.getDomain(), scope.getKey())).thenReturn(Completable.complete());
         when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
 
         TestObserver testObserver = scopeService.delete("my-scope", false).test();