Repository: gravitee-io/graviteeio-access-management
Pull request #542 (2019-03-08 16:38:41)

feat(approval): add user consent duration time per scope
closes gravitee-io/issues#1850


Patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/SecurityDomainRouterFactory.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/SecurityDomainRouterFactory.java
index b27776e122..2fa3dd06e9 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/SecurityDomainRouterFactory.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/SecurityDomainRouterFactory.java
@@ -21,6 +21,7 @@ import io.gravitee.am.gateway.handler.email.EmailManager;
 import io.gravitee.am.gateway.handler.form.FormManager;
 import io.gravitee.am.gateway.handler.oauth2.client.ClientSyncService;
 import io.gravitee.am.gateway.handler.oauth2.granter.extensiongrant.ExtensionGrantManager;
+import io.gravitee.am.gateway.handler.oauth2.scope.ScopeManager;
 import io.gravitee.am.gateway.handler.spring.HandlerConfiguration;
 import io.gravitee.am.gateway.handler.vertx.VertxSecurityDomainHandler;
 import io.gravitee.am.model.Domain;
@@ -96,6 +97,7 @@ public class SecurityDomainRouterFactory {
         components.add(ExtensionGrantManager.class);
         components.add(FormManager.class);
         components.add(EmailManager.class);
+        components.add(ScopeManager.class);
 
         components.forEach(componentClass -> {
             LifecycleComponent lifecyclecomponent = applicationContext.getBean(componentClass);
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 9476e479ed..dcf84438b1 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
@@ -29,5 +29,5 @@ public interface ApprovalService {
 
     Single<AuthorizationRequest> checkApproval(AuthorizationRequest authorizationRequest, Client client, String username);
 
-    Single<AuthorizationRequest> saveApproval(AuthorizationRequest authorizationRequest, String username);
+    Single<AuthorizationRequest> saveApproval(AuthorizationRequest authorizationRequest, Client client, String username);
 }
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 6c7d257437..df129cc631 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
@@ -18,9 +18,11 @@ package io.gravitee.am.gateway.handler.oauth2.approval.impl;
 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.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.oauth2.Scope;
 import io.gravitee.am.model.oauth2.ScopeApproval;
 import io.gravitee.am.repository.oauth2.api.ScopeApprovalRepository;
 import io.reactivex.Observable;
@@ -43,6 +45,9 @@ public class ApprovalServiceImpl implements ApprovalService {
     @Autowired
     private Domain domain;
 
+    @Autowired
+    private ScopeManager scopeManager;
+
     @Value("${oauth2.approval.expiry:-1}")
     private int approvalExpirySeconds;
 
@@ -60,20 +65,19 @@ public class ApprovalServiceImpl implements ApprovalService {
     }
 
     @Override
-    public Single<AuthorizationRequest> saveApproval(AuthorizationRequest authorizationRequest, String username) {
-        // Get the approved scopes
-        Set<String> requestedScopes = authorizationRequest.getScopes();
+    public Single<AuthorizationRequest> saveApproval(AuthorizationRequest authorizationRequest, Client client, String username) {
+        // Get the unapproved requested scopes
+        Set<String> requestedScopes = authorizationRequest.getDeniedScopes();
         Set<String> approvedScopes = new HashSet<>();
         Set<ScopeApproval> approvals = new HashSet<>();
 
-        Date expiry = computeExpiry();
-
         // Store the scopes that have been approved / denied
         Map<String, String> approvalParameters = authorizationRequest.getApprovalParameters();
         for (String requestedScope : requestedScopes) {
             String approvalParameter = requestedScope;
             String value = approvalParameters.get(OAuth2Constants.SCOPE_PREFIX + approvalParameter);
             value = value == null ? "" : value.toLowerCase();
+            Date expiry = computeExpiry(client, requestedScope);
             if ("true".equals(value) || value.startsWith("approve")) {
                 approvedScopes.add(requestedScope);
                 approvals.add(new ScopeApproval(username, authorizationRequest.getClientId(),
@@ -128,6 +132,9 @@ public class ApprovalServiceImpl implements ApprovalService {
                         authorizationRequest.setApproved(true);
                         return Single.just(authorizationRequest);
                     }
+
+                    // set denied scopes for the user consent page
+                    authorizationRequest.setDeniedScopes(requestedScopes.stream().filter(requestedScope -> !approvedScopes.contains(requestedScope)).collect(Collectors.toSet()));
                     return Single.error(new AccessDeniedException("User denied access"));
                 });
     }
@@ -154,8 +161,23 @@ public class ApprovalServiceImpl implements ApprovalService {
         return false;
     }
 
-    private Date computeExpiry() {
+    private Date computeExpiry(Client client, String scope) {
         Calendar expiresAt = Calendar.getInstance();
+
+        // if client has approval settings, apply them
+        if (client.getScopeApprovals() != null && client.getScopeApprovals().containsKey(scope)) {
+            expiresAt.add(Calendar.SECOND, client.getScopeApprovals().get(scope));
+            return expiresAt.getTime();
+        }
+
+        // if domain has approval settings, apply them
+        Scope domainScope = scopeManager.findByKey(scope);
+        if (domainScope != null && domainScope.getExpiresIn() != null) {
+            expiresAt.add(Calendar.SECOND, domainScope.getExpiresIn());
+            return expiresAt.getTime();
+        }
+
+        // default approval time
         if (approvalExpirySeconds == -1) { // use default of 1 month
             expiresAt.add(Calendar.MONTH, 1);
         }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/request/AuthorizationRequest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/request/AuthorizationRequest.java
index d2ccf79b72..54c1407b43 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/request/AuthorizationRequest.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/request/AuthorizationRequest.java
@@ -18,7 +18,9 @@ package io.gravitee.am.gateway.handler.oauth2.request;
 import io.gravitee.am.gateway.handler.oauth2.response.AuthorizationResponse;
 
 import java.io.Serializable;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * @author David BRASSELY (david.brassely at graviteesource.com)
@@ -67,6 +69,8 @@ public class AuthorizationRequest extends BaseRequest implements Serializable {
 
     private Map<String, String> approvalParameters;
 
+    private Set<String> deniedScopes;
+
     public String getResponseType() {
         return responseType;
     }
@@ -115,6 +119,14 @@ public class AuthorizationRequest extends BaseRequest implements Serializable {
         this.approvalParameters = approvalParameters;
     }
 
+    public Set<String> getDeniedScopes() {
+        return deniedScopes;
+    }
+
+    public void setDeniedScopes(Set<String> deniedScopes) {
+        this.deniedScopes = deniedScopes;
+    }
+
     public OAuth2Request createOAuth2Request() {
         OAuth2Request oAuth2Request = new OAuth2Request();
         oAuth2Request.setClientId(getClientId());
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/scope/ScopeManager.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/scope/ScopeManager.java
new file mode 100644
index 0000000000..8d2e01440f
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/scope/ScopeManager.java
@@ -0,0 +1,33 @@
+/**
+ * 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.oauth2.scope;
+
+import io.gravitee.am.model.oauth2.Scope;
+import io.gravitee.common.service.Service;
+
+import java.util.Set;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface ScopeManager extends Service {
+
+    Set<Scope> findAll();
+
+    Scope findByKey(String scopeKey);
+
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/scope/impl/ScopeManagerImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/scope/impl/ScopeManagerImpl.java
new file mode 100644
index 0000000000..b30b38e3aa
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/scope/impl/ScopeManagerImpl.java
@@ -0,0 +1,128 @@
+/**
+ * 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.oauth2.scope.impl;
+
+import io.gravitee.am.gateway.core.event.ScopeEvent;
+import io.gravitee.am.gateway.handler.oauth2.scope.ScopeManager;
+import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.common.event.Payload;
+import io.gravitee.am.model.oauth2.Scope;
+import io.gravitee.am.repository.management.api.ScopeRepository;
+import io.gravitee.common.event.Event;
+import io.gravitee.common.event.EventListener;
+import io.gravitee.common.event.EventManager;
+import io.gravitee.common.service.AbstractService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class ScopeManagerImpl extends AbstractService implements ScopeManager, InitializingBean, EventListener<ScopeEvent, Payload> {
+
+    private static final Logger logger = LoggerFactory.getLogger(ScopeManagerImpl.class);
+    private ConcurrentMap<String, Scope> scopes = new ConcurrentHashMap<>();
+
+    @Autowired
+    private ScopeRepository scopeRepository;
+
+    @Autowired
+    private Domain domain;
+
+    @Autowired
+    private EventManager eventManager;
+
+    @Override
+    public void afterPropertiesSet() {
+        logger.info("Initializing scopes for domain {}", domain.getName());
+        scopeRepository.findByDomain(domain.getId())
+                .subscribe(
+                        scopes -> {
+                            updateScopes(scopes);
+                            logger.info("Scopes loaded for domain {}", domain.getName());
+                        },
+                        error -> logger.error("Unable to initialize scopes for domain {}", domain.getName(), error));
+
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        logger.info("Register event listener for scopes events");
+        eventManager.subscribeForEvents(this, ScopeEvent.class);
+    }
+
+    @Override
+    public void onEvent(Event<ScopeEvent, Payload> event) {
+        if (domain.getId().equals(event.content().getDomain())) {
+            switch (event.type()) {
+                case DEPLOY:
+                case UPDATE:
+                    updateScope(event.content().getId(), event.type());
+                    break;
+                case UNDEPLOY:
+                    removeScope(event.content().getId());
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public Set<Scope> findAll() {
+        return new HashSet<>(scopes.values());
+    }
+
+    @Override
+    public Scope findByKey(String scopeKey) {
+        return scopes.get(scopeKey);
+    }
+
+    private void updateScopes(Set<Scope> scopes) {
+        scopes
+                .stream()
+                .forEach(scope -> {
+                    this.scopes.put(scope.getKey(), scope);
+                    logger.info("Scope {} loaded for domain {}", scope.getKey(), domain.getName());
+                });
+    }
+
+    private void updateScope(String scopeId, ScopeEvent scopeEvent) {
+        final String eventType = scopeEvent.toString().toLowerCase();
+        logger.info("Domain {} has received {} scope event for {}", domain.getName(), eventType, scopeId);
+        scopeRepository.findById(scopeId)
+                .subscribe(
+                        scope -> {
+                            updateScopes(Collections.singleton(scope));
+                            logger.info("Scope {} {}d for domain {}", scopeId, eventType, domain.getName());
+                        },
+                        error -> logger.error("Unable to {} scope for domain {}", eventType, domain.getName(), error),
+                        () -> logger.error("No scope found with id {}", scopeId));
+    }
+
+    private void removeScope(String scopeId) {
+        logger.info("Domain {} has received scope event, delete scope {}", domain.getName(), scopeId);
+        scopes.values().removeIf(scope -> scopeId.equals(scope.getId()));
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/scope/impl/ScopeServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/scope/impl/ScopeServiceImpl.java
index f856e70765..b83acc8551 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/scope/impl/ScopeServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/scope/impl/ScopeServiceImpl.java
@@ -15,10 +15,9 @@
  */
 package io.gravitee.am.gateway.handler.oauth2.scope.impl;
 
+import io.gravitee.am.gateway.handler.oauth2.scope.ScopeManager;
 import io.gravitee.am.gateway.handler.oauth2.scope.ScopeService;
-import io.gravitee.am.model.Domain;
 import io.gravitee.am.model.oauth2.Scope;
-import io.gravitee.am.repository.management.api.ScopeRepository;
 import io.reactivex.Single;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -31,13 +30,10 @@ import java.util.Set;
 public class ScopeServiceImpl implements ScopeService {
 
     @Autowired
-    private ScopeRepository scopeRepository;
-
-    @Autowired
-    private Domain domain;
+    private ScopeManager scopeManager;
 
     @Override
     public Single<Set<Scope>> getAll() {
-        return scopeRepository.findByDomain(domain.getId());
+        return Single.just(scopeManager.findAll());
     }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/spring/OAuth2Configuration.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/spring/OAuth2Configuration.java
index 1641709167..913ecf2498 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/spring/OAuth2Configuration.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/spring/OAuth2Configuration.java
@@ -33,7 +33,9 @@ import io.gravitee.am.gateway.handler.oauth2.introspection.IntrospectionService;
 import io.gravitee.am.gateway.handler.oauth2.introspection.impl.IntrospectionServiceImpl;
 import io.gravitee.am.gateway.handler.oauth2.revocation.RevocationTokenService;
 import io.gravitee.am.gateway.handler.oauth2.revocation.impl.RevocationTokenServiceImpl;
+import io.gravitee.am.gateway.handler.oauth2.scope.ScopeManager;
 import io.gravitee.am.gateway.handler.oauth2.scope.ScopeService;
+import io.gravitee.am.gateway.handler.oauth2.scope.impl.ScopeManagerImpl;
 import io.gravitee.am.gateway.handler.oauth2.scope.impl.ScopeServiceImpl;
 import io.gravitee.am.gateway.handler.oauth2.token.TokenEnhancer;
 import io.gravitee.am.gateway.handler.oauth2.token.TokenService;
@@ -106,4 +108,9 @@ public class OAuth2Configuration {
     public ClientAssertionService clientAssertionService() {
         return new ClientAssertionServiceImpl();
     }
+
+    @Bean
+    public ScopeManager scopeManager() {
+        return new ScopeManagerImpl();
+    }
 }
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 c7e77791d7..6b95ee04bc 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
@@ -25,6 +25,7 @@ import io.gravitee.am.gateway.handler.oauth2.scope.ScopeService;
 import io.gravitee.am.gateway.handler.oauth2.token.TokenService;
 import io.gravitee.am.gateway.handler.oidc.discovery.OpenIDDiscoveryService;
 import io.gravitee.am.gateway.handler.oidc.flow.Flow;
+import io.gravitee.am.gateway.handler.vertx.auth.handler.ClientAssertionAuthHandler;
 import io.gravitee.am.gateway.handler.vertx.auth.handler.ClientBasicAuthHandler;
 import io.gravitee.am.gateway.handler.vertx.auth.handler.ClientCredentialsAuthHandler;
 import io.gravitee.am.gateway.handler.vertx.auth.handler.RedirectAuthHandler;
@@ -32,7 +33,13 @@ import io.gravitee.am.gateway.handler.vertx.auth.handler.impl.ClientChainAuthHan
 import io.gravitee.am.gateway.handler.vertx.auth.provider.ClientAssertionAuthenticationProvider;
 import io.gravitee.am.gateway.handler.vertx.auth.provider.ClientAuthenticationProvider;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.ErrorHandlerEndpoint;
-import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.*;
+import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.AuthorizationEndpointHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.AuthorizationRequestParseClientHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.AuthorizationRequestParseParametersHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.AuthorizationRequestParseRequiredParametersHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.approval.UserApprovalEndpointHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.approval.UserApprovalRequestParseHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.approval.UserApprovalSubmissionEndpointHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.introspection.CheckTokenEndpointHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.introspection.IntrospectionEndpointHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.revocation.RevocationTokenEndpointHandler;
@@ -106,6 +113,7 @@ public class OAuth2Router {
 
         final AuthHandler clientAuthHandler = ChainAuthHandler.newInstance(new ClientChainAuthHandler())
                 .append(ClientCredentialsAuthHandler.create(clientAuthProvider.getDelegate()))
+                .append(ClientAssertionAuthHandler.create(clientAssertionAuthProvider.getDelegate()))
                 .append(ClientBasicAuthHandler.create(clientAuthProvider.getDelegate()));
 
         final AuthHandler userAuthHandler = RedirectAuthHandler.create(
@@ -117,8 +125,9 @@ public class OAuth2Router {
         Handler<RoutingContext> authorizationRequestParseClientHandler = new AuthorizationRequestParseClientHandler(clientSyncService);
         Handler<RoutingContext> authorizationRequestParseParametersHandler = new AuthorizationRequestParseParametersHandler(domain);
         Handler<RoutingContext> authorizeEndpoint = new AuthorizationEndpointHandler(flow, domain);
-        Handler<RoutingContext> authorizeApprovalEndpoint = new AuthorizationApprovalEndpointHandler(approvalService);
-        Handler<RoutingContext> userApprovalEndpoint = new UserApprovalEndpointHandler(clientSyncService, scopeService, thymeleafTemplateEngine);
+        Handler<RoutingContext> userApprovalRequestParseHandler = new UserApprovalRequestParseHandler(clientSyncService);
+        Handler<RoutingContext> userApprovalSubmissionEndpointHandler = new UserApprovalSubmissionEndpointHandler(approvalService);
+        Handler<RoutingContext> userApprovalEndpoint = new UserApprovalEndpointHandler(scopeService, thymeleafTemplateEngine);
         // Token endpoint
         Handler<RoutingContext> tokenEndpoint = new TokenEndpointHandler(tokenGranter);
         Handler<RoutingContext> tokenRequestParseHandler = new TokenRequestParseHandler();
@@ -140,7 +149,8 @@ public class OAuth2Router {
                 .handler(authorizeEndpoint);
         router.route(HttpMethod.POST, "/authorize")
                 .handler(userAuthHandler)
-                .handler(authorizeApprovalEndpoint);
+                .handler(userApprovalRequestParseHandler)
+                .handler(userApprovalSubmissionEndpointHandler);
         router.route(HttpMethod.POST, "/token")
                 .handler(tokenRequestParseHandler)
                 .handler(clientAuthHandler)
@@ -158,6 +168,7 @@ public class OAuth2Router {
                 .handler(clientAuthHandler)
                 .handler(revocationTokenEndpoint);
         router.route(HttpMethod.GET, "/confirm_access")
+                .handler(userApprovalRequestParseHandler)
                 .handler(userApprovalEndpoint);
         router.route(HttpMethod.GET, "/error")
                 .handler(new ErrorHandlerEndpoint(thymeleafTemplateEngine));
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/UserApprovalEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalEndpointHandler.java
similarity index 51%
rename from gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/UserApprovalEndpointHandler.java
rename to gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalEndpointHandler.java
index 2b2b1e419e..98e59b1889 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/UserApprovalEndpointHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalEndpointHandler.java
@@ -13,10 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization;
+package io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.approval;
 
 import io.gravitee.am.gateway.handler.form.FormManager;
-import io.gravitee.am.gateway.handler.oauth2.client.ClientSyncService;
 import io.gravitee.am.gateway.handler.oauth2.request.AuthorizationRequest;
 import io.gravitee.am.gateway.handler.oauth2.scope.ScopeService;
 import io.gravitee.am.gateway.handler.oauth2.utils.OAuth2Constants;
@@ -37,32 +36,28 @@ import java.util.Set;
  */
 public class UserApprovalEndpointHandler implements Handler<RoutingContext>  {
 
+    private static final String CLIENT_CONTEXT_KEY = "client";
     private ThymeleafTemplateEngine engine;
-    private ClientSyncService clientSyncService;
     private ScopeService scopeService;
 
-    public UserApprovalEndpointHandler(ClientSyncService clientSyncService, ScopeService scopeService, ThymeleafTemplateEngine engine) {
-        this.clientSyncService = clientSyncService;
+    public UserApprovalEndpointHandler(ScopeService scopeService, ThymeleafTemplateEngine engine) {
         this.scopeService = scopeService;
         this.engine = engine;
     }
 
     @Override
     public void handle(RoutingContext routingContext) {
-        // TODO use repository instead ?
+        // retrieve client
+        Client client = routingContext.get(CLIENT_CONTEXT_KEY);
+        // retrieve authorization request
         AuthorizationRequest authorizationRequest = routingContext.session().get(OAuth2Constants.AUTHORIZATION_REQUEST);
 
-        if (authorizationRequest == null) {
-            routingContext.response().setStatusCode(400).end("An authorization request is required to handle user approval");
-            return;
-        }
-
-        clientSyncService.findByClientId(authorizationRequest.getClientId())
-                .toSingle()
-                .zipWith(scopeService.getAll(), (client, domainScopes) -> {
+        // fetch scope information (name + description) from the authorization request
+        scopeService.getAll()
+                .map(domainScopes -> {
                     // fetch scope information (name + description) from the authorization request
                     Set<Scope> requestedScopes = new HashSet<>();
-                    for (String requestScope : authorizationRequest.getScopes()) {
+                    for (String requestScope : authorizationRequest.getDeniedScopes()) {
                         Scope requestedScope = domainScopes
                                 .stream()
                                 .filter(scope -> scope.getKey().equalsIgnoreCase(requestScope))
@@ -71,57 +66,34 @@ public class UserApprovalEndpointHandler implements Handler<RoutingContext>  {
 
                         requestedScopes.add(requestedScope);
                     }
-                    return new ApprovalData(client, requestedScopes);
+                    return requestedScopes;
                 })
-                .subscribe(approvalData -> {
-                        routingContext.put("client", approvalData.getClient());
-                        routingContext.put("scopes", approvalData.getScopes());
-                        engine.render(routingContext, getTemplateFileName(approvalData.getClient()), res -> {
-                            if (res.succeeded()) {
-                                routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML);
-                                routingContext.response().end(res.result());
-                            } else {
-                                routingContext.fail(res.cause());
-                            }
-                        });
-                    },
-                    error -> routingContext.fail(error));
-    }
-
-    public void setClientSyncService(ClientSyncService clientSyncService) {
-        this.clientSyncService = clientSyncService;
+                .subscribe(
+                        requestedScopes -> {
+                            Client safeClient = safeClient(client);
+                            routingContext.put("client", safeClient);
+                            routingContext.put("scopes", requestedScopes);
+                            engine.render(routingContext, getTemplateFileName(safeClient), res -> {
+                                if (res.succeeded()) {
+                                    routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML);
+                                    routingContext.response().end(res.result());
+                                } else {
+                                   routingContext.fail(res.cause());
+                                }
+                            });
+                        },
+                        error -> routingContext.fail(error));
     }
 
     private String getTemplateFileName(Client client) {
         return "oauth2_user_consent" + (client != null ? FormManager.TEMPLATE_NAME_SEPARATOR + client.getId() : "");
     }
 
-    private class ApprovalData {
-        private Client client;
-        private Set<Scope> scopes;
-
-        public ApprovalData(Client client, Set<Scope> scopes) {
-            this.client = new Client();
-            this.client.setId(client.getId());
-            this.client.setClientId(client.getClientId());
-            this.client.setClientName(client.getClientName());
-            this.scopes = scopes;
-        }
-
-        public Client getClient() {
-            return client;
-        }
-
-        public void setClient(Client client) {
-            this.client = client;
-        }
-
-        public Set<Scope> getScopes() {
-            return scopes;
-        }
-
-        public void setScopes(Set<Scope> scopes) {
-            this.scopes = scopes;
-        }
+    private Client safeClient(Client client) {
+        Client safeClient = new Client();
+        safeClient.setId(client.getId());
+        safeClient.setClientId(client.getClientId());
+        safeClient.setClientName(client.getClientName());
+        return safeClient;
     }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalRequestParseHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalRequestParseHandler.java
new file mode 100644
index 0000000000..772d60f2c0
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalRequestParseHandler.java
@@ -0,0 +1,84 @@
+/**
+ * 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.oauth2.endpoint.authorization.approval;
+
+import io.gravitee.am.gateway.handler.oauth2.client.ClientSyncService;
+import io.gravitee.am.gateway.handler.oauth2.exception.AccessDeniedException;
+import io.gravitee.am.gateway.handler.oauth2.exception.InvalidRequestException;
+import io.gravitee.am.gateway.handler.oauth2.exception.ServerErrorException;
+import io.gravitee.am.gateway.handler.oauth2.request.AuthorizationRequest;
+import io.gravitee.am.gateway.handler.oauth2.utils.OAuth2Constants;
+import io.gravitee.am.model.Client;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.reactivex.ext.auth.User;
+import io.vertx.reactivex.ext.web.RoutingContext;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class UserApprovalRequestParseHandler implements Handler<RoutingContext> {
+
+    private static final String CLIENT_CONTEXT_KEY = "client";
+    private ClientSyncService clientSyncService;
+
+    public UserApprovalRequestParseHandler(ClientSyncService clientSyncService) {
+        this.clientSyncService = clientSyncService;
+    }
+
+    @Override
+    public void handle(RoutingContext routingContext) {
+        // user must redirected here after an authorization request
+        AuthorizationRequest authorizationRequest = routingContext.session().get(OAuth2Constants.AUTHORIZATION_REQUEST);
+        if (authorizationRequest == null) {
+            routingContext.response().setStatusCode(400).end("An authorization request is required to handle user approval");
+            return;
+        }
+
+        // check client
+        authenticate(authorizationRequest.getClientId(), resultHandler -> {
+            if (resultHandler.failed()) {
+                routingContext.fail(resultHandler.cause());
+                return;
+            }
+            // put client in the execution context
+            Client client = resultHandler.result();
+            routingContext.put(CLIENT_CONTEXT_KEY, client);
+
+            // check user
+            User authenticatedUser = routingContext.user();
+            if (authenticatedUser == null || ! (authenticatedUser.getDelegate() instanceof io.gravitee.am.gateway.handler.vertx.auth.user.User)) {
+                routingContext.fail(new AccessDeniedException());
+                return;
+            }
+            routingContext.next();
+        });
+
+    }
+
+
+    private void authenticate(String clientId, Handler<AsyncResult<Client>> authHandler) {
+        clientSyncService
+                .findByClientId(clientId)
+                .subscribe(
+                        client -> authHandler.handle(Future.succeededFuture(client)),
+                        error -> authHandler.handle(Future.failedFuture(new ServerErrorException("Server error: unable to find client with client_id " + clientId))),
+                        () -> authHandler.handle(Future.failedFuture(new InvalidRequestException("No client found for client_id " + clientId)))
+                );
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationApprovalEndpointHandler.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
similarity index 83%
rename from gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationApprovalEndpointHandler.java
rename to gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/approval/UserApprovalSubmissionEndpointHandler.java
index b174c6c8ea..c9835c6639 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationApprovalEndpointHandler.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
@@ -13,13 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization;
+package io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.approval;
 
 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.model.Client;
 import io.gravitee.common.http.HttpHeaders;
 import io.vertx.core.Handler;
 import io.vertx.reactivex.core.MultiMap;
@@ -45,31 +46,27 @@ import java.util.stream.Collectors;
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
  * @author GraviteeSource Team
  */
-public class AuthorizationApprovalEndpointHandler implements Handler<RoutingContext> {
+public class UserApprovalSubmissionEndpointHandler implements Handler<RoutingContext> {
 
-    private static final Logger logger = LoggerFactory.getLogger(AuthorizationApprovalEndpointHandler.class);
+    private static final Logger logger = LoggerFactory.getLogger(UserApprovalSubmissionEndpointHandler.class);
+    private static final String CLIENT_CONTEXT_KEY = "client";
     private ApprovalService approvalService;
 
-    public AuthorizationApprovalEndpointHandler(ApprovalService approvalService) {
+    public UserApprovalSubmissionEndpointHandler(ApprovalService approvalService) {
         this.approvalService = approvalService;
     }
 
     @Override
     public void handle(RoutingContext 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).
-        User authenticatedUser = context.user();
-        if (authenticatedUser == null || ! (authenticatedUser.getDelegate() instanceof io.gravitee.am.gateway.handler.vertx.auth.user.User)) {
-            throw new AccessDeniedException();
-        }
+        // retrieve client
+        Client client = context.get(CLIENT_CONTEXT_KEY);
 
+        // retrieve end user
+        User authenticatedUser = context.user();
         io.gravitee.am.model.User endUser = ((io.gravitee.am.gateway.handler.vertx.auth.user.User) authenticatedUser.getDelegate()).getUser();
 
+        // retrieve authorization request
         AuthorizationRequest authorizationRequest = context.session().get(OAuth2Constants.AUTHORIZATION_REQUEST);
-        if (authorizationRequest == null) {
-            context.response().setStatusCode(400).end("An authorization request is required to handle user approval");
-            return;
-        }
 
         HttpServerRequest req = context.request();
         if (!req.isExpectMultipart()) {
@@ -87,7 +84,7 @@ public class AuthorizationApprovalEndpointHandler implements Handler<RoutingCont
         authorizationRequest.setApprovalParameters(approvalParameters);
 
         // handle approval response
-        approvalService.saveApproval(authorizationRequest, endUser.getUsername())
+        approvalService.saveApproval(authorizationRequest, client, endUser.getUsername())
                 .subscribe(authorizationRequest1 -> {
                     // user denied access
                     if (!authorizationRequest.isApproved()) {
diff --git a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/upgrades/OpenIDScopeUpgrader.java b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/upgrades/OpenIDScopeUpgrader.java
index 1f82ca08c1..70339f2d1b 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/upgrades/OpenIDScopeUpgrader.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/upgrades/OpenIDScopeUpgrader.java
@@ -85,6 +85,7 @@ public class OpenIDScopeUpgrader implements Upgrader, Ordered {
                         scope.setName(existingScope.getName() != null ? existingScope.getName() : systemScope.getLabel());
                         scope.setDescription(existingScope.getDescription() != null ? existingScope.getDescription() : systemScope.getDescription());
                         scope.setClaims(systemScope.getClaims());
+                        scope.setExpiresIn(existingScope.getExpiresIn());
                         return scopeService.update(domain, optScope.get().getId(), scope);
                     }
                     return Single.just(optScope.get());
diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/Client.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/Client.java
index 61461ef2e4..b75827bfe2 100644
--- a/gravitee-am-model/src/main/java/io/gravitee/am/model/Client.java
+++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/Client.java
@@ -161,7 +161,7 @@ public class Client {
 
     private boolean enhanceScopesWithUserPermissions;
 
-    private boolean useDefaultIdentityProvider;
+    private Map<String, Integer> scopeApprovals;
 
     public String getId() {
         return id;
@@ -598,12 +598,12 @@ public class Client {
         this.enhanceScopesWithUserPermissions = enhanceScopesWithUserPermissions;
     }
 
-    public boolean isUseDefaultIdentityProvider() {
-        return useDefaultIdentityProvider;
+    public Map<String, Integer> getScopeApprovals() {
+        return scopeApprovals;
     }
 
-    public void setUseDefaultIdentityProvider(boolean useDefaultIdentityProvider) {
-        this.useDefaultIdentityProvider = useDefaultIdentityProvider;
+    public void setScopeApprovals(Map<String, Integer> scopeApprovals) {
+        this.scopeApprovals = scopeApprovals;
     }
 
     @Override
diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/oauth2/Scope.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/oauth2/Scope.java
index 332b07d305..a65c42af5a 100644
--- a/gravitee-am-model/src/main/java/io/gravitee/am/model/oauth2/Scope.java
+++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/oauth2/Scope.java
@@ -57,6 +57,11 @@ public class Scope {
 
     private List<String> claims;
 
+    /**
+     * Expiration time for user consent (in seconds)
+     */
+    private Integer expiresIn;
+
     public Scope() {
     }
 
@@ -136,6 +141,14 @@ public class Scope {
         this.claims = claims;
     }
 
+    public Integer getExpiresIn() {
+        return expiresIn;
+    }
+
+    public void setExpiresIn(Integer expiresIn) {
+        this.expiresIn = expiresIn;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoClientRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoClientRepository.java
index ba1668ea9a..940e61181b 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoClientRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoClientRepository.java
@@ -37,9 +37,7 @@ import org.bson.conversions.Bson;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.PostConstruct;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -220,6 +218,7 @@ public class MongoClientRepository extends AbstractManagementMongoRepository imp
         client.setEnhanceScopesWithUserPermissions(clientMongo.isEnhanceScopesWithUserPermissions());
         client.setCreatedAt(clientMongo.getCreatedAt());
         client.setUpdatedAt(clientMongo.getUpdatedAt());
+        client.setScopeApprovals((Map)clientMongo.getScopeApprovals());
         return client;
     }
 
@@ -283,6 +282,7 @@ public class MongoClientRepository extends AbstractManagementMongoRepository imp
         clientMongo.setEnhanceScopesWithUserPermissions(client.isEnhanceScopesWithUserPermissions());
         clientMongo.setCreatedAt(client.getCreatedAt());
         clientMongo.setUpdatedAt(client.getUpdatedAt());
+        clientMongo.setScopeApprovals(client.getScopeApprovals() != null ? new Document((Map)client.getScopeApprovals()) : new Document());
         return clientMongo;
     }
 
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoScopeRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoScopeRepository.java
index 8d97ab336c..1108000cd9 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoScopeRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoScopeRepository.java
@@ -101,6 +101,7 @@ public class MongoScopeRepository extends AbstractManagementMongoRepository impl
         scope.setDomain(scopeMongo.getDomain());
         scope.setSystem(scopeMongo.isSystem());
         scope.setClaims(scopeMongo.getClaims());
+        scope.setExpiresIn(scopeMongo.getExpiresIn());
         scope.setCreatedAt(scopeMongo.getCreatedAt());
         scope.setUpdatedAt(scopeMongo.getUpdatedAt());
 
@@ -120,6 +121,7 @@ public class MongoScopeRepository extends AbstractManagementMongoRepository impl
         scopeMongo.setDomain(scope.getDomain());
         scopeMongo.setSystem(scope.isSystem());
         scopeMongo.setClaims(scope.getClaims());
+        scopeMongo.setExpiresIn(scope.getExpiresIn());
         scopeMongo.setCreatedAt(scope.getCreatedAt());
         scopeMongo.setUpdatedAt(scope.getUpdatedAt());
 
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ClientMongo.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ClientMongo.java
index 2d5d30ada5..dcaeb9fd84 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ClientMongo.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ClientMongo.java
@@ -144,6 +144,11 @@ public class ClientMongo extends Auditable {
 
     private boolean enhanceScopesWithUserPermissions;
 
+    /**
+     * Map codec support is planned for version 3.7 jira.mongodb.org issue: JAVA-2695
+     */
+    private Document scopeApprovals;
+
     public String getId() {
         return id;
     }
@@ -560,6 +565,14 @@ public class ClientMongo extends Auditable {
         this.enhanceScopesWithUserPermissions = enhanceScopesWithUserPermissions;
     }
 
+    public Document getScopeApprovals() {
+        return scopeApprovals;
+    }
+
+    public void setScopeApprovals(Document scopeApprovals) {
+        this.scopeApprovals = scopeApprovals;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ScopeMongo.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ScopeMongo.java
index f040f50ebc..474f4cef81 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ScopeMongo.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ScopeMongo.java
@@ -40,12 +40,8 @@ public class ScopeMongo extends Auditable {
 
     private String name;
 
-    private Map<String, String> names;
-
     private String description;
 
-    private Map<String, String> descriptions;
-
     /**
      * Security domain associated to the scope
      */
@@ -55,6 +51,8 @@ public class ScopeMongo extends Auditable {
 
     private List<String> claims;
 
+    private Integer expiresIn;
+
     public String getId() {
         return id;
     }
@@ -111,6 +109,13 @@ public class ScopeMongo extends Auditable {
         this.claims = claims;
     }
 
+    public Integer getExpiresIn() {
+        return expiresIn;
+    }
+
+    public void setExpiresIn(Integer expiresIn) {
+        this.expiresIn = expiresIn;
+    }
 
     @Override
     public boolean equals(Object o) {
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/EmailTemplateService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/EmailTemplateService.java
index f572244405..2aa61cc45c 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/EmailTemplateService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/EmailTemplateService.java
@@ -34,8 +34,6 @@ public interface EmailTemplateService {
 
     Single<List<Email>> findByDomain(String domain);
 
-    Single<List<Email>> findByDomainAndClient(String domain, String client);
-
     Maybe<Email> findByDomainAndTemplate(String domain, String template);
 
     Maybe<Email> findByDomainAndClientAndTemplate(String domain, String client, String template);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java
index 1ce79d79b9..6ce18bf855 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java
@@ -42,10 +42,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import java.net.URI;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -362,6 +359,7 @@ public class ClientServiceImpl implements ClientService {
                     client.setIdTokenCustomClaims(updateClient.getIdTokenCustomClaims());
                     client.setCertificate(updateClient.getCertificate());
                     client.setEnhanceScopesWithUserPermissions(updateClient.isEnhanceScopesWithUserPermissions());
+                    client.setScopeApprovals(updateClient.getScopeApprovals());
                     return client;
                 })
                 .map(ResponseTypeUtils::applyDefaultResponseType)
@@ -468,15 +466,28 @@ public class ClientServiceImpl implements ClientService {
                         }
                     }
 
-                    //Check scopes.
-                    return scopeService.validateScope(domainId, client.getScopes());
+                    // check scopes and scope approvals
+                    return scopeService.validateScope(domainId, client.getScopes())
+                            .map(isValid -> {
+                                // scopes are valid, let's check scope approvals
+                                if (isValid && client.getScopeApprovals() != null) {
+                                    Map<String, Integer> scopeApprovals = client.getScopeApprovals()
+                                            .entrySet()
+                                            .stream()
+                                            .filter(entry -> client.getScopes() != null && client.getScopes().contains(entry.getKey()))
+                                            .collect(Collectors.toMap(
+                                                    entry -> entry.getKey(),
+                                                    entry -> entry.getValue()));
+                                    client.setScopeApprovals(scopeApprovals);
+                                }
+                                return isValid;
+                            });
                 })
                 .flatMap(isValid -> {
                     if (!isValid) {
                         //last boolean come from scopes validation...
                         return Single.error(new InvalidClientMetadataException("non valid scopes"));
                     }
-
                     //ensure correspondance between response & grant types.
                     GrantTypeUtils.completeGrantTypeCorrespondance(client);
 
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/EmailTemplateServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/EmailTemplateServiceImpl.java
index 2619e469df..06ebdaa24b 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/EmailTemplateServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/EmailTemplateServiceImpl.java
@@ -88,18 +88,6 @@ public class EmailTemplateServiceImpl implements EmailTemplateService {
                 });
     }
 
-    @Override
-    public Single<List<Email>> findByDomainAndClient(String domain, String client) {
-        LOGGER.debug("Find email by domain {} and client {}", domain, client);
-        return emailRepository.findByDomainAndClient(domain, client)
-                .onErrorResumeNext(ex -> {
-                    LOGGER.error("An error occurs while trying to find a email using its domain {} and client {}", domain, client, ex);
-                    return Single.error(new TechnicalManagementException(
-                            String.format("An error occurs while trying to find a email using its domain %s and client %s", domain, client), ex));
-                });
-    }
-
-
     @Override
     public Maybe<Email> findByDomainAndClientAndTemplate(String domain, String client, String template) {
         LOGGER.debug("Find email by domain {}, client {} and template {}", domain, client, template);
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 06b40f606b..354c66f137 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
@@ -16,10 +16,15 @@
 package io.gravitee.am.service.impl;
 
 import io.gravitee.am.common.utils.RandomString;
+import io.gravitee.am.model.common.event.Action;
+import io.gravitee.am.model.common.event.Event;
+import io.gravitee.am.model.common.event.Payload;
+import io.gravitee.am.model.common.event.Type;
 import io.gravitee.am.model.oauth2.Scope;
 import io.gravitee.am.repository.management.api.ScopeRepository;
 import io.gravitee.am.repository.oauth2.api.ScopeApprovalRepository;
 import io.gravitee.am.service.ClientService;
+import io.gravitee.am.service.DomainService;
 import io.gravitee.am.service.RoleService;
 import io.gravitee.am.service.ScopeService;
 import io.gravitee.am.service.exception.*;
@@ -65,6 +70,9 @@ public class ScopeServiceImpl implements ScopeService {
     @Autowired
     private ClientService clientService;
 
+    @Autowired
+    private DomainService domainService;
+
     @Override
     public Maybe<Scope> findById(String id) {
         LOGGER.debug("Find scope by ID: {}", id);
@@ -82,21 +90,27 @@ public class ScopeServiceImpl implements ScopeService {
         String scopeKey = newScope.getKey().toLowerCase();
         return scopeRepository.findByDomainAndKey(domain, scopeKey)
                 .isEmpty()
-                    .flatMap(empty -> {
-                        if (!empty) {
-                            throw new ScopeAlreadyExistsException(scopeKey, domain);
-                        }
-                        Scope scope = new Scope();
-                        scope.setId(RandomString.generate());
-                        scope.setDomain(domain);
-                        scope.setKey(scopeKey);
-                        scope.setName(newScope.getName());
-                        scope.setDescription(newScope.getDescription());
-                        scope.setCreatedAt(new Date());
-                        scope.setUpdatedAt(new Date());
+                .flatMap(empty -> {
+                    if (!empty) {
+                        throw new ScopeAlreadyExistsException(scopeKey, domain);
+                    }
+                    Scope scope = new Scope();
+                    scope.setId(RandomString.generate());
+                    scope.setDomain(domain);
+                    scope.setKey(scopeKey);
+                    scope.setName(newScope.getName());
+                    scope.setDescription(newScope.getDescription());
+                    scope.setExpiresIn(newScope.getExpiresIn());
+                    scope.setCreatedAt(new Date());
+                    scope.setUpdatedAt(new Date());
 
-                        return scopeRepository.create(scope);
-                    })
+                    return scopeRepository.create(scope);
+                })
+                .flatMap(scope -> {
+                    // Reload domain to take care about scope creation
+                    Event event = new Event(Type.SCOPE, new Payload(scope.getId(), scope.getDomain(), Action.CREATE));
+                    return domainService.reload(domain, event).flatMap(domain1 -> Single.just(scope));
+                })
                 .onErrorResumeNext(ex -> {
                     if (ex instanceof AbstractManagementException) {
                         return Single.error(ex);
@@ -125,10 +139,16 @@ public class ScopeServiceImpl implements ScopeService {
                     scope.setClaims(newScope.getClaims());
                     scope.setName(newScope.getName());
                     scope.setDescription(newScope.getDescription());
+                    scope.setExpiresIn(newScope.getExpiresIn());
                     scope.setCreatedAt(new Date());
                     scope.setUpdatedAt(new Date());
                     return scopeRepository.create(scope);
                 })
+                .flatMap(scope -> {
+                    // Reload domain to take care about scope creation
+                    Event event = new Event(Type.SCOPE, new Payload(scope.getId(), scope.getDomain(), Action.CREATE));
+                    return domainService.reload(domain, event).flatMap(domain1 -> Single.just(scope));
+                })
                 .onErrorResumeNext(ex -> {
                     if (ex instanceof AbstractManagementException) {
                         return Single.error(ex);
@@ -146,10 +166,16 @@ public class ScopeServiceImpl implements ScopeService {
                 .flatMapSingle(scope -> {
                     scope.setName(updateScope.getName());
                     scope.setDescription(updateScope.getDescription());
+                    scope.setExpiresIn(updateScope.getExpiresIn());
                     scope.setUpdatedAt(new Date());
 
                     return scopeRepository.update(scope);
                 })
+                .flatMap(scope -> {
+                    // Reload domain to take care about scope update
+                    Event event = new Event(Type.SCOPE, new Payload(scope.getId(), scope.getDomain(), Action.UPDATE));
+                    return domainService.reload(domain, event).flatMap(domain1 -> Single.just(scope));
+                })
                 .onErrorResumeNext(ex -> {
                     if (ex instanceof AbstractManagementException) {
                         return Single.error(ex);
@@ -170,8 +196,14 @@ public class ScopeServiceImpl implements ScopeService {
                     scope.setUpdatedAt(new Date());
                     scope.setSystem(true);
                     scope.setClaims(updateScope.getClaims());
+                    scope.setExpiresIn(updateScope.getExpiresIn());
                     return scopeRepository.update(scope);
                 })
+                .flatMap(scope -> {
+                    // Reload domain to take care about scope update
+                    Event event = new Event(Type.SCOPE, new Payload(scope.getId(), scope.getDomain(), Action.UPDATE));
+                    return domainService.reload(domain, event).flatMap(domain1 -> Single.just(scope));
+                })
                 .onErrorResumeNext(ex -> {
                     if (ex instanceof AbstractManagementException) {
                         return Single.error(ex);
@@ -226,7 +258,13 @@ public class ScopeServiceImpl implements ScopeService {
                                 // 3_ Remove scopes from scope_approvals
                                 .andThen(scopeApprovalRepository.delete(scope.getDomain(), scope.getKey()))
                                 // 4_ Delete scope
-                                .andThen(scopeRepository.delete(scopeId)))
+                                .andThen(scopeRepository.delete(scopeId))
+                                // 5_ reload domain
+                                .andThen(
+                                        Completable.fromSingle(
+                                                domainService.reload(scope.getDomain(),
+                                                        new Event(Type.SCOPE, new Payload(scope.getId(), scope.getDomain(), Action.DELETE))))
+                                ))
                                 .onErrorResumeNext(ex -> {
                                     if (ex instanceof AbstractManagementException) {
                                         return Completable.error(ex);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/NewScope.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/NewScope.java
index 46a895d794..53b2ff54a6 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/NewScope.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/NewScope.java
@@ -32,6 +32,8 @@ public class NewScope {
     @NotNull
     private String description;
 
+    private Integer expiresIn;
+
     public String getKey() {
         return key;
     }
@@ -55,4 +57,12 @@ public class NewScope {
     public void setDescription(String description) {
         this.description = description;
     }
+
+    public Integer getExpiresIn() {
+        return expiresIn;
+    }
+
+    public void setExpiresIn(Integer expiresIn) {
+        this.expiresIn = expiresIn;
+    }
 }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/NewSystemScope.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/NewSystemScope.java
index ad04404cfb..c7d5f44099 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/NewSystemScope.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/NewSystemScope.java
@@ -35,6 +35,8 @@ public class NewSystemScope {
 
     private List<String> claims;
 
+    private Integer expiresIn;
+
     public String getKey() {
         return key;
     }
@@ -66,4 +68,12 @@ public class NewSystemScope {
     public void setClaims(List<String> claims) {
         this.claims = claims;
     }
-}
\ No newline at end of file
+
+    public Integer getExpiresIn() {
+        return expiresIn;
+    }
+
+    public void setExpiresIn(Integer expiresIn) {
+        this.expiresIn = expiresIn;
+    }
+}
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java
index f40ecb64f5..32baafecb6 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java
@@ -75,6 +75,7 @@ public class PatchClient {
     private Optional<Set<String>> oauth2Identities;
     private Optional<String> certificate;
     private Optional<Boolean> enhanceScopesWithUserPermissions;
+    private Optional<Map<String, Integer>> scopeApprovals;
 
     public Optional<List<String>> getRedirectUris() {
         return redirectUris;
@@ -460,6 +461,14 @@ public class PatchClient {
         this.enhanceScopesWithUserPermissions = enhanceScopesWithUserPermissions;
     }
 
+    public Optional<Map<String, Integer>> getScopeApprovals() {
+        return scopeApprovals;
+    }
+
+    public void setScopeApprovals(Optional<Map<String, Integer>> scopeApprovals) {
+        this.scopeApprovals = scopeApprovals;
+    }
+
     public Client patch(Client toPatch, boolean forceNull) {
 
         SetterUtils.safeSet(toPatch::setRedirectUris, this.getRedirectUris());
@@ -515,6 +524,7 @@ public class PatchClient {
             SetterUtils.safeSet(toPatch::setCertificate, this.getCertificate());
         }
         SetterUtils.safeSet(toPatch::setEnhanceScopesWithUserPermissions, this.getEnhanceScopesWithUserPermissions(), boolean.class);
+        SetterUtils.safeSet(toPatch::setScopeApprovals, this.getScopeApprovals());
 
         return toPatch;
     }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateClient.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateClient.java
index d2d129110a..2707888689 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateClient.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateClient.java
@@ -53,6 +53,8 @@ public class UpdateClient {
 
     private boolean enhanceScopesWithUserPermissions;
 
+    private Map<String, Integer> scopeApprovals;
+
     public List<String> getRedirectUris() {
         return redirectUris;
     }
@@ -164,4 +166,12 @@ public class UpdateClient {
     public void setEnhanceScopesWithUserPermissions(boolean enhanceScopesWithUserPermissions) {
         this.enhanceScopesWithUserPermissions = enhanceScopesWithUserPermissions;
     }
+
+    public Map<String, Integer> getScopeApprovals() {
+        return scopeApprovals;
+    }
+
+    public void setScopeApprovals(Map<String, Integer> scopeApprovals) {
+        this.scopeApprovals = scopeApprovals;
+    }
 }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateScope.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateScope.java
index 6075f24005..54e86d7db5 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateScope.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateScope.java
@@ -27,12 +27,10 @@ public class UpdateScope {
     @NotNull
     private String name;
 
-    private Map<String, String> names;
-
     @NotNull
     private String description;
 
-    private Map<String, String> descriptions;
+    private Integer expiresIn;
 
     public String getName() {
         return name;
@@ -42,14 +40,6 @@ public class UpdateScope {
         this.name = name;
     }
 
-    public Map<String, String> getNames() {
-        return names;
-    }
-
-    public void setNames(Map<String, String> names) {
-        this.names = names;
-    }
-
     public String getDescription() {
         return description;
     }
@@ -58,11 +48,11 @@ public class UpdateScope {
         this.description = description;
     }
 
-    public Map<String, String> getDescriptions() {
-        return descriptions;
+    public Integer getExpiresIn() {
+        return expiresIn;
     }
 
-    public void setDescriptions(Map<String, String> descriptions) {
-        this.descriptions = descriptions;
+    public void setExpiresIn(Integer expiresIn) {
+        this.expiresIn = expiresIn;
     }
 }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateSystemScope.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateSystemScope.java
index ce48881b4f..de08615535 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateSystemScope.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateSystemScope.java
@@ -32,6 +32,8 @@ public class UpdateSystemScope {
 
     private List<String> claims;
 
+    private Integer expiresIn;
+
     public String getName() {
         return name;
     }
@@ -55,4 +57,12 @@ public class UpdateSystemScope {
     public void setClaims(List<String> claims) {
         this.claims = claims;
     }
-}
\ No newline at end of file
+
+    public Integer getExpiresIn() {
+        return expiresIn;
+    }
+
+    public void setExpiresIn(Integer expiresIn) {
+        this.expiresIn = expiresIn;
+    }
+}
diff --git a/gravitee-am-ui/src/app/app.module.ts b/gravitee-am-ui/src/app/app.module.ts
index 0c96f32e36..d98b20f34e 100644
--- a/gravitee-am-ui/src/app/app.module.ts
+++ b/gravitee-am-ui/src/app/app.module.ts
@@ -173,6 +173,7 @@ import { EmailComponent, EmailInfoDialog } from "./domain/components/emails/emai
 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";
 
 @NgModule({
   declarations: [
@@ -270,7 +271,8 @@ import { SelectClientsComponent } from "./domain/components/clients/select-clien
     EmailsComponent,
     EmailComponent,
     EmailInfoDialog,
-    SelectClientsComponent
+    SelectClientsComponent,
+    ClientScopeComponent
   ],
   imports: [
     BrowserModule,
diff --git a/gravitee-am-ui/src/app/domain/clients/client/scopes/client-scope.component.html b/gravitee-am-ui/src/app/domain/clients/client/scopes/client-scope.component.html
new file mode 100644
index 0000000000..13eeabbf07
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/clients/client/scopes/client-scope.component.html
@@ -0,0 +1,49 @@
+<!--
+
+    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.
+
+-->
+<form fxLayout="row" #clientScopeForm="ngForm" style="padding: 20px; border: 1px solid black; margin-top: 10px;">
+  <div fxFlex>
+    <mat-form-field fxFlex style="margin-right: 20px;">
+      <mat-select placeholder="Scope" name="key" [(ngModel)]="scope.key" [formControl]="selectScopes">
+        <mat-select-trigger>{{selectScopes.value ? selectScopes.value : ''}}</mat-select-trigger>
+        <mat-option *ngFor="let scope of scopes" [value]="scope.key">
+          <mat-icon style="vertical-align:middle;">donut_large</mat-icon>
+          <span>{{ scope.key }}</span> | <small>{{scope.name}}</small>
+        </mat-option>
+      </mat-select>
+    </mat-form-field>
+    <mat-form-field fxFlex style="margin-right: 20px;">
+      <input matInput type="number" placeholder="Duration time" name="expiresIn" min="0" [min]="0" [(ngModel)]="scope.expiresIn">
+      <mat-hint>Optional. The duration for the user consent.</mat-hint>
+    </mat-form-field>
+    <mat-form-field fxFlex style="margin-right: 20px;">
+      <mat-select placeholder="Unit time" name="unitTime" [(ngModel)]="scope.unitTime">
+        <mat-option value="seconds">SECONDS</mat-option>
+        <mat-option value="minutes">MINUTES</mat-option>
+        <mat-option value="hours">HOURS</mat-option>
+        <mat-option value="days">DAYS</mat-option>
+        <mat-option value="weeks">WEEKS</mat-option>
+        <mat-option value="months">MONTHS</mat-option>
+        <mat-option value="years">YEARS</mat-option>
+      </mat-select>
+      <mat-hint>Optional. Unit time for the user consent duration.</mat-hint>
+    </mat-form-field>
+  </div>
+  <div>
+    <button style="margin-top: 10px;" mat-stroked-button [disabled]="formIsInvalid()" (click)="addScope(scope)">+ ADD</button>
+  </div>
+</form>
diff --git a/gravitee-am-ui/src/app/domain/clients/client/scopes/client-scope.component.ts b/gravitee-am-ui/src/app/domain/clients/client/scopes/client-scope.component.ts
new file mode 100644
index 0000000000..cd9e55465a
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/clients/client/scopes/client-scope.component.ts
@@ -0,0 +1,50 @@
+/*
+ * 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, EventEmitter, Input, Output, ViewChild} from '@angular/core'
+import {FormControl, NgForm} from "@angular/forms";
+
+@Component({
+  selector: 'client-scope',
+  templateUrl: './client-scope.component.html'
+})
+
+export class ClientScopeComponent {
+  scope: any = {};
+  selectScopes = new FormControl();
+  @Output() addScopeChange = new EventEmitter();
+  @Input() scopes: any[] = [];
+  @ViewChild('clientScopeForm') form: NgForm;
+
+  constructor() {}
+
+  addScope() {
+    this.addScopeChange.emit(this.scope);
+    this.scope = {};
+    this.form.reset(this.scope);
+  }
+
+  formIsInvalid() {
+    if (!this.scope.key) {
+      return true;
+    }
+
+    if ((this.scope.expiresIn > 0 && !this.scope.unitTime) || (this.scope.expiresIn <= 0 && this.scope.unitTime)) {
+      return true;
+    }
+
+    return false;
+  }
+}
diff --git a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html
index 7ae3da581c..f9db07beda 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html
+++ b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html
@@ -42,9 +42,9 @@
       <input matInput type="text" placeholder="Client name" name="client_name" [(ngModel)]="client.clientName">
       <mat-hint>The client human readable name.</mat-hint>
     </mat-form-field>
-    <div fxLayout="row">
+    <div fxLayout="row" class="client-oauth-zone">
       <section class="type-section">
-        <h4 style="margin-bottom: 5px;">Grant types</h4>
+        <h4>Grant types</h4>
         <mat-hint style="font-size: 75%; margin-bottom: 20px;">OAuth 2.0 grant types used by the client to obtain an access token.<br> Each grant type is specific to a type of application.</mat-hint>
         <mat-checkbox class="type-selection" *ngFor="let grantType of grantTypes"
                       name="grantTypes"
@@ -53,9 +53,16 @@
                       (change)="selectedGrantType($event)">
               {{grantType.name}}
         </mat-checkbox>
+        <mat-checkbox *ngFor="let extensionGrant of customGrantTypes"
+                      name="customGrantTypes"
+                      value="{{extensionGrant.value}}"
+                      [checked]="extensionGrant.checked"
+                      (change)="selectedCustomGrantType($event)">
+          <span style="text-transform: uppercase;">{{extensionGrant.name}}</span>
+        </mat-checkbox>
       </section>
       <section class="type-section">
-        <h4 style="margin-bottom: 5px;">Response types</h4>
+        <h4>Response types</h4>
         <mat-hint style="font-size: 75%; margin-bottom: 20px;">OAuth 2.0 response types used by the client to obtain an access token.<br> Each response type must correspond to a selected grant type</mat-hint>
         <mat-checkbox class="type-selection" *ngFor="let responseType of responseTypes"
                       name="responseTypes"
@@ -66,101 +73,114 @@
         </mat-checkbox>
       </section>
     </div>
-    <div>
-      <h5>Extension grants</h5>
-      <div fxLayout="column">
-        <mat-checkbox *ngFor="let extensionGrant of customGrantTypes"
-                     name="customGrantTypes"
-                     value="{{extensionGrant.value}}"
-                     [checked]="extensionGrant.checked"
-                     (change)="selectedCustomGrantType($event)">
-          <span style="text-transform: uppercase;">{{extensionGrant.name}}</span>
-        </mat-checkbox>
-      </div>
-    </div>
 
-    <h4 style="margin-bottom: 0px;">Redirect URIs</h4>
-    <mat-chip-list style="margin-top: 10px; margin-bottom: 10px;" class="mat-chip-list-stacked">
-      <mat-chip *ngFor="let redirectURI of client.redirectUris" color="accent" selected="true" fxLayout="row">
-        <span fxFlex style="margin-top: 3px;">{{redirectURI}}</span>
-        <mat-icon class="delete-chip" (click)="removeRedirectURI(redirectURI, $event)">clear</mat-icon>
-      </mat-chip>
-    </mat-chip-list>
-    <mat-form-field fxFlex>
-      <input matInput #inputRedirectURI (keyup.enter)="addRedirectURI($event, inputRedirectURI)" placeholder="New redirect URI..."/>
-      <mat-hint>
+    <div class="client-oauth-zone" fxLayout="column">
+      <h4>Redirect URIs</h4>
+      <mat-hint style="font-size: 75%;">
         Set of allowed URLs where the user will be redirected after successfully login to your client.
-        It prevents some vulnerabilities like being redirected to unsafe websites. Once written, press enter to add the redirect URI.
+        It prevents some vulnerabilities like being redirected to unsafe websites.
       </mat-hint>
-    </mat-form-field>
-
-    <br>
-
-    <h4 style="margin-bottom: 5px;">Scopes</h4>
-      <mat-hint style="font-size: 75%; margin-bottom: 20px;">Let you limit client's access to protected resources/APIs.</mat-hint>
-      <mat-form-field>
-        <mat-chip-list style="margin-top: 10px; margin-bottom: 10px;" #chipListPermissions>
-          <mat-chip *ngFor="let scope of selectedScopes" color="accent"
-                    selected="true" (remove)="removeScope(scope)">
-            <mat-icon style="vertical-align:middle;">donut_large</mat-icon>
-            &nbsp;<span>{{scope.key}}</span>&nbsp;|&nbsp;<small>{{scope.name}}</small>
-            <mat-icon matChipRemove>cancel</mat-icon>
-          </mat-chip>
-          <input matInput
-                 placeholder="Permissions"
-                 aria-label="Permissions"
-                 [matAutocomplete]="auto"
-                 [matChipInputFor]="chipListPermissions"
-                 #chipInput>
-        </mat-chip-list>
-
-      <mat-autocomplete #auto="matAutocomplete" (optionSelected)="addScope($event)">
-        <mat-option *ngFor="let scope of scopes" [value]="scope.key">
-          <mat-icon style="vertical-align:middle;">donut_large</mat-icon>
-          <span>{{ scope.key }}</span> | <small>{{scope.name}}</small>
-        </mat-option>
-      </mat-autocomplete>
-    </mat-form-field>
+      <mat-chip-list style="margin-top: 10px;" class="mat-chip-list-stacked">
+        <mat-chip *ngFor="let redirectURI of client.redirectUris" color="accent" selected="true" fxLayout="row">
+          <span fxFlex style="margin-top: 3px;">{{redirectURI}}</span>
+          <mat-icon class="delete-chip" (click)="removeRedirectURI(redirectURI, $event)">clear</mat-icon>
+        </mat-chip>
+      </mat-chip-list>
+      <mat-form-field fxFlex>
+        <input matInput #inputRedirectURI (keyup.enter)="addRedirectURI($event, inputRedirectURI)" placeholder="New redirect URI..."/>
+        <mat-hint>Once written, press enter to add the redirect URI.</mat-hint>
+      </mat-form-field>
+    </div>
 
-    <h4>Auto approval</h4>
-    <mat-slide-toggle
-      (change)="enableAutoApprove($event)"
-      [checked]="isAutoApprove()">
-    </mat-slide-toggle>
-    <mat-hint style="font-size: 75%;">Allow to skip user approval. Generally used for internal trusted service applications.</mat-hint>
+    <div class="client-oauth-zone client-scopes" fxLayout="column">
+      <h4>Scopes</h4>
+      <div fxLayout="column">
+        <mat-hint style="font-size: 75%;">Let you limit client's access to protected resources/APIs.</mat-hint>
+        <client-scope [scopes]="scopes" (addScopeChange)="addScope($event)"></client-scope>
+        <ngx-datatable style="margin-top: 20px;" *ngIf="!isSelectedScopesEmpty"
+                       [columnMode]="'flex'"
+                       class="material"
+                       [headerHeight]="40"
+                       [footerHeight]="40"
+                       [rowHeight]="55"
+                       [rows]='selectedScopes'>
+          <ngx-datatable-column name="Scope" [flexGrow]="1">
+            <ng-template let-row="row" let-rowIndex="rowIndex" ngx-datatable-cell-template>
+              <mat-icon style="vertical-align:middle;">donut_large</mat-icon>
+              <span>{{ row.key }}</span> | <small>{{row.name}}</small>
+            </ng-template>
+          </ngx-datatable-column>
+          <ngx-datatable-column name="User consent expiry period" [flexGrow]="1">
+            <ng-template let-row="row" let-rowIndex="rowIndex" ngx-datatable-cell-template>
+              <div>
+                <span *ngIf="!(scopeApprovalExists(row.key))" style="color: grey; font-size: 85%; font-style: italic;">No time set</span>
+                <span *ngIf="(scopeApprovalExists(row.key))">{{ getScopeApproval(row.key) }}</span>
+              </div>
+              <div>
+                <small style="color: grey;">
+                  ( default value :
+                  <span *ngIf="!row.expiresIn" style="font-style: italic;">No time set</span>
+                  <span *ngIf="row.expiresIn">{{ getScopeExpiry(row.expiresIn) }}</span>
+                  )
+                </small>
+              </div>
+            </ng-template>
+          </ngx-datatable-column>
+          <ngx-datatable-column name="Actions" [flexGrow]="1">
+            <ng-template let-row="row" ngx-datatable-cell-template>
+              <div fxLayout="row" class="client-scope-actions">
+                <button mat-icon-button (click)="removeScope(row.key, $event)"><mat-icon>clear</mat-icon></button>
+              </div>
+            </ng-template>
+          </ngx-datatable-column>
+        </ngx-datatable>
+      </div>
+      <div fxLayout="column">
+        <h5 style="margin: 10px 0 10px 0;">Enhance scopes</h5>
+        <mat-slide-toggle
+          (change)="enhanceScopesWithUserPermissions($event)"
+          [checked]="isScopesEnhanceWithUserPermissions()">
+        </mat-slide-toggle>
+        <mat-hint style="font-size: 75%;">Enhance client scopes with user permissions. Useful for fine tuned resources access.</mat-hint>
+      </div>
+    </div>
 
-    <h5 style="margin: 10px 0 10px 0;">Enhance scopes</h5>
-    <mat-slide-toggle
-      (change)="enhanceScopesWithUserPermissions($event)"
-      [checked]="isScopesEnhanceWithUserPermissions()">
-    </mat-slide-toggle>
-    <mat-hint style="font-size: 75%;">Enhance client scopes with user permissions. Useful for fine tuned resources access.</mat-hint>
+    <div class="client-oauth-zone" fxLayout="column">
+      <h4>User consent</h4>
+      <mat-slide-toggle
+        (change)="enableAutoApprove($event)"
+        [checked]="isAutoApprove()">
+      </mat-slide-toggle>
+      <mat-hint style="font-size: 75%;">Allow to skip user approval. Generally used for internal trusted service applications.</mat-hint>
+    </div>
 
-    <br>
+    <div class="client-oauth-zone" fxLayout="column">
+      <h4>Tokens</h4>
+      <div fxLayout="row">
+        <mat-form-field fxFlex style="margin-right: 20px;">
+          <mat-icon matPrefix>timer</mat-icon>
+          <input matInput type="number" placeholder="Access token validity" name="accessTokenValidity" [(ngModel)]="client.accessTokenValiditySeconds" />
+          <span matSuffix>seconds</span>
+        </mat-form-field>
+        <mat-form-field fxFlex>
+          <mat-icon matPrefix>timer</mat-icon>
+          <input matInput type="number" placeholder="Refresh token validity" name="refreshTokenValidity" [(ngModel)]="client.refreshTokenValiditySeconds" />
+          <span matSuffix>seconds</span>
+        </mat-form-field>
+      </div>
+    </div>
 
-    <h4>Tokens</h4>
-    <div fxLayout="row">
-      <mat-form-field fxFlex style="margin-right: 20px;">
-        <mat-icon matPrefix>timer</mat-icon>
-        <input matInput type="number" placeholder="Access token validity" name="accessTokenValidity" [(ngModel)]="client.accessTokenValiditySeconds" />
-        <span matSuffix>seconds</span>
-      </mat-form-field>
-      <mat-form-field fxFlex>
-        <mat-icon matPrefix>timer</mat-icon>
-        <input matInput type="number" placeholder="Refresh token validity" name="refreshTokenValidity" [(ngModel)]="client.refreshTokenValiditySeconds" />
-        <span matSuffix>seconds</span>
+    <div class="client-oauth-zone" fxLayout="column">
+      <h4>Certificate</h4>
+      <mat-form-field>
+        <mat-select placeholder="Certificate" name="certificate" [(ngModel)]="client.certificate">
+          <mat-option><em>None</em></mat-option>
+          <mat-option *ngFor="let certificate of certificates" [value]="certificate.id">{{ certificate.name }}</mat-option>
+        </mat-select>
+        <mat-hint>Certificate use to sign tokens.</mat-hint>
       </mat-form-field>
     </div>
 
-    <h4>Certificate</h4>
-    <mat-form-field>
-      <mat-select placeholder="Certificate" name="certificate" [(ngModel)]="client.certificate">
-        <mat-option><em>None</em></mat-option>
-        <mat-option *ngFor="let certificate of certificates" [value]="certificate.id">{{ certificate.name }}</mat-option>
-      </mat-select>
-      <mat-hint style="font-size: 75%;">Certificate use to sign tokens.</mat-hint>
-    </mat-form-field>
-
     <div fxLayout="row" class="client-settings-form-actions">
       <button mat-raised-button [disabled]="(!clientForm.valid || clientForm.pristine) && !formChanged" type="submit">SAVE</button>
     </div>
diff --git a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss
index f635efda6c..db132c3ccd 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss
+++ b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss
@@ -62,3 +62,31 @@ mat-icon.delete-chip {
   display: inline-flex;
   width: 100%;
 }
+
+.client-oauth-zone {
+  padding: 10px;
+  background-color: #f1f1f1;
+  margin-top: 20px;
+  h4 {
+    margin: 5px 0;
+  }
+}
+
+.client-scopes {
+  a {
+    cursor: pointer;
+    font-size: 75%;
+    color: #2196f3;
+  }
+
+  ngx-datatable {
+    background: transparent;
+    box-shadow: none;
+  }
+
+  .client-scope-actions {
+    button {
+      top: -7px;
+    }
+  }
+}
diff --git a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts
index 72d416c879..71ac769803 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts
+++ b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts
@@ -13,15 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
 import { ClientService } from "../../../../services/client.service";
 import { SnackbarService } from "../../../../services/snackbar.service";
 import { ProviderService } from "../../../../services/provider.service";
 import { ActivatedRoute, Router } from "@angular/router";
 import { CertificateService } from "../../../../services/certificate.service";
 import { DialogService } from "../../../../services/dialog.service";
-import { MatInput } from "@angular/material/input";
 import * as _ from 'lodash';
+import * as moment from "moment";
 
 export interface Scope {
   id: string;
@@ -36,10 +36,11 @@ export interface Scope {
 })
 export class ClientSettingsComponent implements OnInit {
 
-  @ViewChild('chipInput') chipInput: MatInput;
+  @ViewChild('dynamic', { read: ViewContainerRef }) viewContainerRef: ViewContainerRef;
 
   private domainId: string;
   selectedScopes: Scope[];
+  selectedScopeApprovals: any;
   client: any;
   formChanged: boolean = false;
   identityProviders: any[] = [];
@@ -60,9 +61,13 @@ export class ClientSettingsComponent implements OnInit {
   ];
   customGrantTypes: any[];
 
-  constructor(private clientService: ClientService, private snackbarService: SnackbarService,
-              private providerService: ProviderService, private certificateService: CertificateService,
-              private route: ActivatedRoute, private router: Router, private dialogService: DialogService) {
+  constructor(private clientService: ClientService,
+              private snackbarService: SnackbarService,
+              private providerService: ProviderService,
+              private certificateService: CertificateService,
+              private route: ActivatedRoute,
+              private router: Router,
+              private dialogService: DialogService) {
   }
 
   ngOnInit() {
@@ -70,6 +75,7 @@ export class ClientSettingsComponent implements OnInit {
     this.client = this.route.snapshot.parent.data['client'];
     this.customGrantTypes = this.route.snapshot.data['domainGrantTypes'];
     this.scopes = this.route.snapshot.data['scopes'];
+    this.selectedScopeApprovals = this.client.scopeApprovals || {};
     (!this.client.redirectUris) ? this.client.redirectUris = [] : this.client.redirectUris = this.client.redirectUris;
     (!this.client.scopes) ? this.client.scopes = [] : this.client.scopes = this.client.scopes;
     this.providerService.findByDomain(this.domainId).map(res => res.json()).subscribe(data => this.identityProviders = data);
@@ -195,18 +201,24 @@ export class ClientSettingsComponent implements OnInit {
     return this.client.autoApproveScopes && this.client.autoApproveScopes.indexOf('true') > -1;
   }
 
-  addScope(event) {
-    this.selectedScopes = this.selectedScopes.concat(_.remove(this.scopes, { 'key': event.option.value }));
-    this.chipInput['nativeElement'].blur();
+  addScope(scope) {
+    this.selectedScopes = this.selectedScopes.concat(_.remove(this.scopes, { 'key': scope.key }));
+    this.selectedScopes = [...this.selectedScopes];
+
+    if (scope.expiresIn && scope.unitTime) {
+      this.selectedScopeApprovals[scope.key] = moment.duration(scope.expiresIn, scope.unitTime).asSeconds();
+    }
+
     this.formChanged = true;
   }
 
-  removeScope(scope) {
+  removeScope(scopeKey, event) {
+    event.preventDefault();
     this.scopes = this.scopes.concat(_.remove(this.selectedScopes, function(selectPermission) {
-      return selectPermission.key === scope.key;
+      return selectPermission.key === scopeKey;
     }));
-
-    this.chipInput['nativeElement'].blur();
+    this.selectedScopes = [...this.selectedScopes];
+    delete this.selectedScopeApprovals[scopeKey];
     this.formChanged = true;
   }
 
@@ -219,12 +231,31 @@ export class ClientSettingsComponent implements OnInit {
     return this.client.enhanceScopesWithUserPermissions;
   }
 
+  get isSelectedScopesEmpty() {
+    return !this.selectedScopes || this.selectedScopes.length == 0;
+  }
+
+  getScopeApproval(scopeKey) {
+    let scopeApproval = this.selectedScopeApprovals[scopeKey];
+    return this.getScopeExpiry(scopeApproval);
+  }
+
+  getScopeExpiry(expiresIn) {
+    return (expiresIn) ? moment.duration(expiresIn, 'seconds').humanize() : 'no time set';
+  }
+
+  scopeApprovalExists(scopeKey) {
+    return this.selectedScopeApprovals.hasOwnProperty(scopeKey);
+  }
+
   update() {
     this.client.authorizedGrantTypes = this.selectedGrantTypes.concat(this.selectedCustomGrantTypes);
     this.client.responseTypes = this.selectedResponseTypes;
     this.client.scopes = _.map(this.selectedScopes, scope => scope.key);
+    this.client.scopeApprovals = this.selectedScopeApprovals;
     this.clientService.update(this.domainId, this.client.id, this.client).map(res => res.json()).subscribe(data => {
       this.client = data;
+      this.formChanged = false;
       this.snackbarService.open("Client updated");
       this.initGrantTypes();
       this.initResponseTypes();
diff --git a/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.html b/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.html
index f0d8305347..f3f2f27b38 100644
--- a/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.html
+++ b/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.html
@@ -33,9 +33,38 @@
         <input matInput type="text" placeholder="Description" name="description" [(ngModel)]="scope.description" required>
         <mat-hint>A description for your scope.</mat-hint>
       </mat-form-field>
+
+      <div fxLayout="column" class="user-consent-zone">
+        <h4>User consent</h4>
+        <small>Expiry/lapsing period for consent after which the user would need to confirm that scope.</small>
+        <p fxLayout="row" style="align-items: center; font-size: 90%;">
+          <mat-icon mat-list-icon>alarm</mat-icon>
+          <span>Current period :&nbsp;</span>
+          <span *ngIf="!scope.expiresIn" style="color: grey; font-style: italic;">No time set</span>
+          <span *ngIf="scope.expiresIn">{{ getScopeExpiry() }}</span>
+        </p>
+        <div fxLayout="row">
+          <mat-form-field fxFlex style="margin-right: 20px;">
+            <input matInput type="number" placeholder="Duration time" name="expiresIn" min="0" [min]="0" [(ngModel)]="expiresIn">
+            <mat-hint>The duration for the user consent.</mat-hint>
+          </mat-form-field>
+          <mat-form-field fxFlex style="margin-right: 20px;">
+            <mat-select placeholder="Unit time" name="unitTime" [(ngModel)]="unitTime">
+              <mat-option value="seconds">SECONDS</mat-option>
+              <mat-option value="minutes">MINUTES</mat-option>
+              <mat-option value="hours">HOURS</mat-option>
+              <mat-option value="days">DAYS</mat-option>
+              <mat-option value="weeks">WEEKS</mat-option>
+              <mat-option value="months">MONTHS</mat-option>
+              <mat-option value="years">YEARS</mat-option>
+            </mat-select>
+            <mat-hint>Unit time for the user consent duration.</mat-hint>
+          </mat-form-field>
+        </div>
+      </div>
       <div fxLayout="row" fxLayoutAlign="end" style="margin-top: 50px;">
         <a mat-button [routerLink]="['..']" style="margin-right: 20px;">CANCEL</a>
-        <button mat-raised-button [disabled]="!scopeForm.valid || scopeForm.pristine" type="submit">CREATE</button>
+        <button mat-raised-button [disabled]="(!scopeForm.valid || scopeForm.pristine) || formIsInvalid()" type="submit">CREATE</button>
       </div>
     </form>
     <div class="new-scope-description" fxFlex>
diff --git a/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.scss b/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.scss
index a32445d466..bec1901409 100644
--- a/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.scss
+++ b/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.scss
@@ -10,6 +10,18 @@
       background: #fafafa;
       border-radius: 2px;
       border: 1px solid rgb(226, 229, 231);
+      .user-consent-zone {
+        padding: 10px;
+        background-color: #f1f1f1;
+        margin-top: 20px;
+        h4 {
+          margin: 5px 0;
+        }
+        small {
+          font-size: 75%;
+          color: rgba(0, 0, 0, 0.54);
+        }
+      }
       .scope-form-actions {
         margin-top: 20px;
       }
diff --git a/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.ts b/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.ts
index 207cbeafad..7cb89bb81c 100644
--- a/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/scopes/creation/scope-creation.component.ts
@@ -18,6 +18,7 @@ import { ScopeService } from "../../../../services/scope.service";
 import { ActivatedRoute, Router } from "@angular/router";
 import { SnackbarService } from "../../../../services/snackbar.service";
 import {AppConfig} from "../../../../../config/app.config";
+import * as moment from "moment";
 
 @Component({
   selector: 'app-creation',
@@ -28,6 +29,8 @@ export class ScopeCreationComponent implements OnInit {
   private domainId: string;
   private adminContext: boolean;
   scope: any = {};
+  expiresIn: any;
+  unitTime: any;
 
   constructor(private scopeService: ScopeService, private router: Router, private route: ActivatedRoute,
               private snackbarService : SnackbarService) { }
@@ -43,6 +46,10 @@ export class ScopeCreationComponent implements OnInit {
   create() {
     // Force lowercase for scope key
     this.scope.key = this.scope.key.toLowerCase();
+    // set duration time for user consent
+    if (this.expiresIn && this.unitTime) {
+      this.scope.expiresIn = moment.duration(this.expiresIn, this.unitTime).asSeconds();
+    }
     this.scopeService.create(this.domainId, this.scope).map(res => res.json()).subscribe(data => {
       this.snackbarService.open("Scope " + data.name + " created");
       if (this.adminContext) {
@@ -53,4 +60,11 @@ export class ScopeCreationComponent implements OnInit {
     });
   }
 
+  formIsInvalid() {
+    return (this.expiresIn > 0 && !this.unitTime) || (this.expiresIn <= 0 && this.unitTime);
+  }
+
+  getScopeExpiry() {
+    return (this.scope.expiresIn) ? moment.duration(this.scope.expiresIn, 'seconds').humanize() : 'no time set';
+  }
 }
diff --git a/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.html b/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.html
index 2619f7e9ef..c66ef77fc3 100644
--- a/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.html
+++ b/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.html
@@ -31,8 +31,37 @@
         <input matInput type="text" placeholder="Description" name="description" [(ngModel)]="scope.description" required>
       </mat-form-field>
 
-      <div fxLayout="row" class="scope-scope-actions">
-        <button mat-raised-button [disabled]="(!scopeForm.valid || scopeForm.pristine) && !formChanged" type="submit">SAVE</button>
+      <div fxLayout="column" class="user-consent-zone">
+        <h4>User consent</h4>
+        <small>Expiry/lapsing period for consent after which the user would need to confirm that scope.</small>
+        <p fxLayout="row" style="align-items: center; font-size: 90%;">
+          <mat-icon mat-list-icon>alarm</mat-icon>
+          <span>Current period :&nbsp;</span>
+          <span *ngIf="!scope.expiresIn" style="color: grey; font-style: italic;">No time set</span>
+          <span *ngIf="scope.expiresIn">{{ getScopeExpiry() }} <button mat-icon-button (click)="clearExpiry()" matTooltip="Clear expiry periode"><mat-icon>clear</mat-icon></button></span>
+        </p>
+        <div fxLayout="row">
+          <mat-form-field fxFlex style="margin-right: 20px;">
+            <input matInput type="number" placeholder="Duration time" name="expiresIn" min="0" [min]="0" [(ngModel)]="expiresIn">
+            <mat-hint>The duration for the user consent.</mat-hint>
+          </mat-form-field>
+          <mat-form-field fxFlex style="margin-right: 20px;">
+            <mat-select placeholder="Unit time" name="unitTime" [(ngModel)]="unitTime">
+              <mat-option value="seconds">SECONDS</mat-option>
+              <mat-option value="minutes">MINUTES</mat-option>
+              <mat-option value="hours">HOURS</mat-option>
+              <mat-option value="days">DAYS</mat-option>
+              <mat-option value="weeks">WEEKS</mat-option>
+              <mat-option value="months">MONTHS</mat-option>
+              <mat-option value="years">YEARS</mat-option>
+            </mat-select>
+            <mat-hint>Unit time for the user consent duration.</mat-hint>
+          </mat-form-field>
+        </div>
+      </div>
+
+      <div fxLayout="row" class="scope-form-actions">
+        <button mat-raised-button [disabled]="(!scopeForm.valid || scopeForm.pristine || formIsInvalid()) && !formChanged" type="submit">SAVE</button>
       </div>
     </form>
 
diff --git a/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.scss b/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.scss
index bea82248c9..ff7d8fe5ad 100644
--- a/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.scss
+++ b/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.scss
@@ -10,6 +10,18 @@
       background: #fafafa;
       border-radius: 2px;
       border: 1px solid rgb(226, 229, 231);
+      .user-consent-zone {
+        padding: 10px;
+        background-color: #f1f1f1;
+        margin-top: 20px;
+        h4 {
+          margin: 5px 0;
+        }
+        small {
+          font-size: 75%;
+          color: rgba(0, 0, 0, 0.54);
+        }
+      }
       .scope-form-actions {
         margin-top: 20px;
       }
diff --git a/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.ts b/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.ts
index 267096a63e..8cb259b071 100644
--- a/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/scopes/scope/scope.component.ts
@@ -13,11 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Component, OnInit } from '@angular/core';
+import {Component, OnInit, ViewChild} from '@angular/core';
 import { ActivatedRoute } from "@angular/router";
 import { ScopeService } from "../../../../services/scope.service";
 import { SnackbarService } from "../../../../services/snackbar.service";
 import { BreadcrumbService } from "../../../../../libraries/ng2-breadcrumb/components/breadcrumbService";
+import * as moment from "moment";
+import {NgForm} from "@angular/forms";
 
 @Component({
   selector: 'app-scope',
@@ -28,6 +30,9 @@ export class ScopeComponent implements OnInit {
   private domainId: string;
   scope: any;
   formChanged: boolean = false;
+  expiresIn: any;
+  unitTime: any;
+  @ViewChild('scopeForm') public scopeForm: NgForm;
 
   constructor(private scopeService: ScopeService, private snackbarService: SnackbarService, private route: ActivatedRoute,
               private breadcrumbService: BreadcrumbService) { }
@@ -41,10 +46,17 @@ export class ScopeComponent implements OnInit {
   update() {
     // Force lowercase for scope key
     this.scope.key = this.scope.key.toLowerCase();
+    // set duration time for user consent
+    if (this.expiresIn && this.unitTime) {
+      this.scope.expiresIn = moment.duration(this.expiresIn, this.unitTime).asSeconds();
+    }
     this.scopeService.update(this.domainId, this.scope.id, this.scope).map(res => res.json()).subscribe(data => {
       this.scope = data;
       this.initBreadcrumb();
       this.formChanged = false;
+      this.scopeForm.reset(this.scope);
+      this.expiresIn = null;
+      this.unitTime = null;
       this.snackbarService.open("Scope updated");
     });
   }
@@ -52,4 +64,17 @@ export class ScopeComponent implements OnInit {
   initBreadcrumb() {
     this.breadcrumbService.addFriendlyNameForRouteRegex('/domains/'+this.domainId+'/settings/scopes/'+this.scope.id+'$', this.scope.name);
   }
+
+  formIsInvalid() {
+    return (this.expiresIn > 0 && !this.unitTime) || (this.expiresIn <= 0 && this.unitTime);
+  }
+
+  getScopeExpiry() {
+    return (this.scope.expiresIn) ? moment.duration(this.scope.expiresIn, 'seconds').humanize() : 'no time set';
+  }
+
+  clearExpiry() {
+    this.scope.expiresIn = null;
+    this.formChanged = true;
+  }
 }
diff --git a/gravitee-am-ui/src/app/domain/settings/scopes/scopes.component.html b/gravitee-am-ui/src/app/domain/settings/scopes/scopes.component.html
index ef0b85eaea..338c478ed1 100644
--- a/gravitee-am-ui/src/app/domain/settings/scopes/scopes.component.html
+++ b/gravitee-am-ui/src/app/domain/settings/scopes/scopes.component.html
@@ -38,6 +38,12 @@
           {{row.name}}
         </ng-template>
       </ngx-datatable-column>
+      <ngx-datatable-column name="User consent expiry period" [flexGrow]="1">
+        <ng-template let-row="row" ngx-datatable-cell-template>
+          <span *ngIf="!row.expiresIn" style="color: grey; font-style: italic;">No time set</span>
+          <span *ngIf="row.expiresIn">{{ getScopeExpiry(row.expiresIn) }}</span>
+        </ng-template>
+      </ngx-datatable-column>
       <ngx-datatable-column name="Actions" [flexGrow]="1">
         <ng-template let-row="row" ngx-datatable-cell-template>
           <div fxLayout="row" class="scopes-actions">
diff --git a/gravitee-am-ui/src/app/domain/settings/scopes/scopes.component.ts b/gravitee-am-ui/src/app/domain/settings/scopes/scopes.component.ts
index a07a6e75a3..82b6d8dff4 100644
--- a/gravitee-am-ui/src/app/domain/settings/scopes/scopes.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/scopes/scopes.component.ts
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 import { Component, OnInit } from '@angular/core';
-import { ProviderService } from "../../../services/provider.service";
 import { SnackbarService } from "../../../services/snackbar.service";
 import { DialogService } from "../../../services/dialog.service";
 import { ActivatedRoute } from "@angular/router";
 import { ScopeService } from "../../../services/scope.service";
+import * as moment from 'moment';
 
 @Component({
   selector: 'app-scopes',
@@ -60,4 +60,8 @@ export class DomainSettingsScopesComponent implements OnInit {
       });
   }
 
+  getScopeExpiry(expiresIn) {
+    return expiresIn ? moment.duration(expiresIn, 'seconds').humanize() : 'no time set';
+  }
+
 }
diff --git a/gravitee-am-ui/src/app/services/client.service.ts b/gravitee-am-ui/src/app/services/client.service.ts
index 0bf92da198..c3096813d8 100644
--- a/gravitee-am-ui/src/app/services/client.service.ts
+++ b/gravitee-am-ui/src/app/services/client.service.ts
@@ -56,7 +56,8 @@ export class ClientService {
       'oauth2Identities': client.oauth2Identities,
       'certificate': client.certificate,
       'enhanceScopesWithUserPermissions' : client.enhanceScopesWithUserPermissions,
-      'responseTypes' : client.responseTypes
+      'responseTypes' : client.responseTypes,
+      'scopeApprovals' : client.scopeApprovals
     });
   }
 
diff --git a/gravitee-am-ui/src/app/services/scope.service.ts b/gravitee-am-ui/src/app/services/scope.service.ts
index ce4888122d..162be4c7cb 100644
--- a/gravitee-am-ui/src/app/services/scope.service.ts
+++ b/gravitee-am-ui/src/app/services/scope.service.ts
@@ -40,7 +40,7 @@ export class ScopeService {
     return this.http.put(this.scopes + domainId + "/scopes/" + id, {
       'name' : scope.name,
       'description' : scope.description,
-      'permissions' : scope.permissions
+      'expiresIn' : scope.expiresIn
     });
   }
 


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 0b7e19cc68..e180ca2f20 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
@@ -19,6 +19,7 @@ import io.gravitee.am.gateway.handler.oauth2.approval.impl.ApprovalServiceImpl;
 import io.gravitee.am.gateway.handler.oauth2.client.ClientSyncService;
 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.scope.ScopeManager;
 import io.gravitee.am.gateway.handler.oauth2.utils.OAuth2Constants;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
@@ -58,6 +59,9 @@ public class ApprovalServiceTest {
     @Mock
     private Domain domain;
 
+    @Mock
+    private ScopeManager scopeManager;
+
     @Test
     public void shouldApproveRequest_clientAutoApproval() {
         final String clientId = "client_id";
@@ -175,7 +179,7 @@ public class ApprovalServiceTest {
 
         AuthorizationRequest authorizationRequest = new AuthorizationRequest();
         authorizationRequest.setClientId(clientId);
-        authorizationRequest.setScopes(new HashSet<>(Arrays.asList(readScope, writeScope)));
+        authorizationRequest.setDeniedScopes(new HashSet<>(Arrays.asList(readScope, writeScope)));
 
         Map<String, String> approvalParameters = new HashMap<>();
         approvalParameters.put(OAuth2Constants.SCOPE_PREFIX + readScope, "true");
@@ -184,7 +188,7 @@ public class ApprovalServiceTest {
 
         when(scopeApprovalRepository.upsert(any())).thenReturn(Single.just(new ScopeApproval()));
 
-        TestObserver<AuthorizationRequest> testObserver = approvalService.saveApproval(authorizationRequest, userId).test();
+        TestObserver<AuthorizationRequest> testObserver = approvalService.saveApproval(authorizationRequest, client, userId).test();
         testObserver.awaitTerminalEvent();
 
         testObserver.assertComplete();
@@ -206,7 +210,7 @@ public class ApprovalServiceTest {
 
         AuthorizationRequest authorizationRequest = new AuthorizationRequest();
         authorizationRequest.setClientId(clientId);
-        authorizationRequest.setScopes(new HashSet<>(Arrays.asList(readScope, writeScope)));
+        authorizationRequest.setDeniedScopes(new HashSet<>(Arrays.asList(readScope, writeScope)));
 
         Map<String, String> approvalParameters = new HashMap<>();
         approvalParameters.put(OAuth2Constants.SCOPE_PREFIX + readScope, "false");
@@ -215,7 +219,7 @@ public class ApprovalServiceTest {
 
         when(scopeApprovalRepository.upsert(any())).thenReturn(Single.just(new ScopeApproval()));
 
-        TestObserver<AuthorizationRequest> testObserver = approvalService.saveApproval(authorizationRequest, userId).test();
+        TestObserver<AuthorizationRequest> testObserver = approvalService.saveApproval(authorizationRequest, client, userId).test();
         testObserver.awaitTerminalEvent();
 
         testObserver.assertComplete();
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 34c61fd023..6eace574e1 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
@@ -16,6 +16,7 @@
 package io.gravitee.am.service;
 
 import io.gravitee.am.model.Client;
+import io.gravitee.am.model.Domain;
 import io.gravitee.am.model.Role;
 import io.gravitee.am.model.oauth2.Scope;
 import io.gravitee.am.repository.exceptions.TechnicalException;
@@ -70,6 +71,9 @@ public class ScopeServiceTest {
     @Mock
     private ScopeApprovalRepository scopeApprovalRepository;
 
+    @Mock
+    private DomainService domainService;
+
     private final static String DOMAIN = "domain1";
 
     @Test
@@ -130,6 +134,7 @@ public class ScopeServiceTest {
         when(newScope.getKey()).thenReturn("my-scope");
         when(scopeRepository.findByDomainAndKey(DOMAIN, "my-scope")).thenReturn(Maybe.empty());
         when(scopeRepository.create(any(Scope.class))).thenReturn(Single.just(new Scope()));
+        when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
 
         TestObserver testObserver = scopeService.create(DOMAIN, newScope).test();
         testObserver.awaitTerminalEvent();
@@ -139,6 +144,7 @@ public class ScopeServiceTest {
 
         verify(scopeRepository, times(1)).findByDomainAndKey(anyString(), anyString());
         verify(scopeRepository, times(1)).create(any(Scope.class));
+        verify(domainService, times(1)).reload(any(), any());
     }
 
     @Test
@@ -147,6 +153,7 @@ public class ScopeServiceTest {
         when(newScope.getKey()).thenReturn("MY-SCOPE");
         when(scopeRepository.findByDomainAndKey(DOMAIN, "my-scope")).thenReturn(Maybe.empty());
         when(scopeRepository.create(any(Scope.class))).thenReturn(Single.just(new Scope()));
+        when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
 
         TestObserver testObserver = scopeService.create(DOMAIN, newScope).test();
         testObserver.awaitTerminalEvent();
@@ -162,6 +169,7 @@ public class ScopeServiceTest {
                 return scope.getKey().equals("my-scope");
             }
         }));
+        verify(domainService, times(1)).reload(any(), any());
     }
 
     @Test
@@ -250,6 +258,7 @@ public class ScopeServiceTest {
         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(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
 
         TestObserver testObserver = scopeService.delete("my-scope", false).test();
         testObserver.awaitTerminalEvent();
@@ -260,6 +269,7 @@ public class ScopeServiceTest {
         verify(roleService, times(1)).findByDomain(DOMAIN);
         verify(clientService, times(1)).findByDomain(DOMAIN);
         verify(scopeRepository, times(1)).delete("my-scope");
+        verify(domainService, times(1)).reload(any(), any());
     }
 
     @Test
@@ -283,6 +293,7 @@ public class ScopeServiceTest {
         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(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
 
         TestObserver testObserver = scopeService.delete("my-scope", false).test();
         testObserver.awaitTerminalEvent();
@@ -295,6 +306,7 @@ public class ScopeServiceTest {
         verify(roleService, times(1)).update(anyString(), anyString(), any(UpdateRole.class));
         verify(clientService, times(1)).patch(anyString(), anyString(), any(PatchClient.class));
         verify(scopeRepository, times(1)).delete("my-scope");
+        verify(domainService, times(1)).reload(any(), any());
     }
 
     @Test