Repository: gravitee-io/graviteeio-access-management
Pull request #595 (2019-06-10 09:43:15)

feat(registration): add an option to complete account when user renewed his password
closes gravitee-io/issues#2345


Patch:
diff --git a/Makefile b/Makefile
index 6f9050b7c2..3a27cfd922 100644
--- a/Makefile
+++ b/Makefile
@@ -160,7 +160,10 @@ postman: ## Run postman non regression test (require newman npm module)
 	@newman run postman/collections/graviteeio-am-openid-core-collection.json -e postman/environment/dev.json --ignore-redirects --insecure --bail
 	@newman run postman/collections/graviteeio-am-openid-dcr-collection.json -e postman/environment/dev.json --ignore-redirects --insecure --bail
 	@newman run postman/collections/graviteeio-am-scim-collection.json -e postman/environment/dev.json --ignore-redirects --insecure --bail
+	@newman run postman/collections/graviteeio-am-scope-management-collection.json -e postman/environment/dev.json --ignore-redirects --insecure --bail
 	@newman run postman/collections/graviteeio-am-client-management-collection.json -e postman/environment/dev.json --ignore-redirects --insecure --bail
+	@newman run postman/collections/graviteeio-am-policies-collection.json -e postman/environment/dev.json --ignore-redirects --insecure --bail
+	@newman run postman/collections/graviteeio-am-login-collection.json -e postman/environment/dev.json --ignore-redirects --insecure --bail
 	@newman run postman/collections/graviteeio-am-user-management-collection.json -e postman/environment/dev.json --ignore-redirects --insecure --bail
 
 oidctest-run: oidctest-install oidctest-start ## Run openid-certification tools, using same docker network
diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/audit/EventType.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/audit/EventType.java
index 8bc7aa8b38..19cc45e88d 100644
--- a/gravitee-am-common/src/main/java/io/gravitee/am/common/audit/EventType.java
+++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/audit/EventType.java
@@ -40,6 +40,8 @@ public interface EventType {
     String USER_DELETED = "USER_DELETED";
     String USER_LOCKED = "USER_LOCKED";
     String USER_UNLOCKED = "USER_UNLOCKED";
+    String USER_ENABLED = "USER_ENABLED";
+    String USER_DISABLED = "USER_DISABLED";
     String USER_PASSWORD_RESET = "USER_PASSWORD_RESET";
     String USER_REGISTERED = "USER_REGISTERED";
     String USER_CONSENT_CONSENTED = "USER_CONSENT_CONSENTED";
@@ -167,7 +169,7 @@ public interface EventType {
     String POLICY_DELETED = "POLICY_DELETED";
 
     static Collection<String> types() {
-        return new TreeSet(Arrays.asList(USER_LOGIN, USER_LOGOUT, USER_CREATED, USER_UPDATED, USER_DELETED, USER_LOCKED, USER_UNLOCKED, USER_PASSWORD_RESET,
+        return new TreeSet(Arrays.asList(USER_LOGIN, USER_LOGOUT, USER_CREATED, USER_UPDATED, USER_DELETED, USER_LOCKED, USER_UNLOCKED, USER_ENABLED, USER_DISABLED, USER_PASSWORD_RESET,
                 USER_REGISTERED, USER_CONSENT_CONSENTED, USER_CONSENT_REVOKED, REGISTRATION_CONFIRMATION, REGISTRATION_CONFIRMATION_REQUESTED, FORGOT_PASSWORD_REQUESTED, DOMAIN_CREATED, DOMAIN_UPDATED,
                 DOMAIN_DELETED, CLIENT_CREATED, CLIENT_UPDATED, CLIENT_DELETED, CLIENT_SECRET_RENEWED, CERTIFICATE_CREATED, CERTIFICATE_UPDATED, CERTIFICATE_DELETED,
                 EMAIL_TEMPLATE_CREATED, EMAIL_TEMPLATE_UPDATED, EMAIL_TEMPLATE_DELETED, FORM_TEMPLATE_CREATED, FORM_TEMPLATE_UPDATED, FORM_TEMPLATE_DELETED,
diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java
index b775d2198c..745bfe7943 100644
--- a/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java
+++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java
@@ -34,30 +34,32 @@ public enum Scope {
             StandardClaims.NAME, StandardClaims.FAMILY_NAME, StandardClaims.GIVEN_NAME, StandardClaims.MIDDLE_NAME,
             StandardClaims.NICKNAME, StandardClaims.PREFERRED_USERNAME, StandardClaims.PROFILE, StandardClaims.PICTURE,
             StandardClaims.WEBSITE, StandardClaims.GENDER, StandardClaims.BIRTHDATE, StandardClaims.ZONEINFO, StandardClaims.LOCALE,
-            StandardClaims.UPDATED_AT)),
-    EMAIL("email","Email","Access to the email and email_verified Claims", Arrays.asList(StandardClaims.EMAIL, StandardClaims.EMAIL_VERIFIED)),
-    ADDRESS("address", "Address","Access to the address Claim", Arrays.asList(StandardClaims.ADDRESS)),
-    PHONE("phone", "Phone", "Access to the phone_number and phone_number_verified Claims", Arrays.asList(StandardClaims.PHONE_NUMBER, StandardClaims.PHONE_NUMBER_VERIFIED)),
-    OPENID("openid","Openid","Used to perform Openid requests", Collections.emptyList()),
-    OFFLINE_ACCESS("offline_access", "Offline_access","Access to End-User UserInfo even when he is not logged in.", Collections.emptyList()),
-    DCR("dcr", "Client_registration", "Access to client information through openid register endpoint.", Collections.emptyList()),
-    DCR_ADMIN("dcr_admin", "Client_registration_admin", "Access to Dynamic Client Registration endpoint.", Collections.emptyList()),
-    SCIM("scim", "SCIM", "Access to System for Cross-domain Identity Management endpoint.", Collections.emptyList()),
-    CONSENT_ADMIN("consent_admin", "Consent_admin", "Access to End-User consents", Collections.emptyList());
+            StandardClaims.UPDATED_AT),true),
+    EMAIL("email","Email","Access to the email and email_verified Claims", Arrays.asList(StandardClaims.EMAIL, StandardClaims.EMAIL_VERIFIED),true),
+    ADDRESS("address", "Address","Access to the address Claim", Arrays.asList(StandardClaims.ADDRESS),true),
+    PHONE("phone", "Phone", "Access to the phone_number and phone_number_verified Claims", Arrays.asList(StandardClaims.PHONE_NUMBER, StandardClaims.PHONE_NUMBER_VERIFIED),true),
+    OPENID("openid","Openid","Used to perform Openid requests", Collections.emptyList(),true),
+    OFFLINE_ACCESS("offline_access", "Offline_access","Access to End-User UserInfo even when he is not logged in.", Collections.emptyList(),true),
+    DCR("dcr", "Client_registration", "Access to client information through openid register endpoint.", Collections.emptyList(),false),
+    DCR_ADMIN("dcr_admin", "Client_registration_admin", "Access to Dynamic Client Registration endpoint.", Collections.emptyList(),false),
+    SCIM("scim", "SCIM", "Access to System for Cross-domain Identity Management endpoint.", Collections.emptyList(),false),
+    CONSENT_ADMIN("consent_admin", "Consent_admin", "Access to End-User consents", Collections.emptyList(),false);
 
     private final String key;
     private final String label;
     private final String description;
     private final List<String> claims;
+    private final boolean discovery;
 
     //See https://tools.ietf.org/html/rfc6749#section-3.3
     public final static String SCOPE_DELIMITER = " ";
 
-    Scope(String key, String label, String description, List<String> claims) {
+    Scope(String key, String label, String description, List<String> claims, boolean discovery) {
         this.key = key;
         this.label = label;
         this.description = description;
         this.claims = claims;
+        this.discovery = discovery;
     }
 
     public String getKey() {
@@ -84,4 +86,8 @@ public enum Scope {
             return false;
         }
     }
+
+    public boolean isDiscovery() {
+        return discovery;
+    }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/password/ForgotPasswordSubmissionEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/password/ForgotPasswordSubmissionEndpoint.java
index fdb64cf0bf..d03a3a0eb1 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/password/ForgotPasswordSubmissionEndpoint.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/password/ForgotPasswordSubmissionEndpoint.java
@@ -17,13 +17,14 @@ package io.gravitee.am.gateway.handler.root.resources.endpoint.user.password;
 
 import io.gravitee.am.common.jwt.Claims;
 import io.gravitee.am.common.oauth2.Parameters;
-import io.gravitee.am.gateway.handler.root.service.user.UserService;
 import io.gravitee.am.gateway.handler.root.resources.handler.user.UserRequestHandler;
+import io.gravitee.am.gateway.handler.root.service.user.UserService;
 import io.gravitee.am.identityprovider.api.DefaultUser;
 import io.gravitee.am.identityprovider.api.User;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
 import io.gravitee.am.service.exception.UserNotFoundException;
+import io.gravitee.am.service.exception.authentication.AccountStatusException;
 import io.vertx.reactivex.ext.web.RoutingContext;
 
 import java.util.HashMap;
@@ -61,6 +62,9 @@ public class ForgotPasswordSubmissionEndpoint extends UserRequestHandler {
                             if (error instanceof UserNotFoundException) {
                                 requestParams.put("warning", "user_not_found");
                                 redirectToPage(context, requestParams);
+                            } else if (error instanceof AccountStatusException) {
+                                requestParams.put("warning", ((AccountStatusException) error).getErrorCode());
+                                redirectToPage(context, requestParams);
                             } else {
                                 requestParams.put("error", error.getMessage());
                                 redirectToPage(context, requestParams, error);
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/password/ResetPasswordSubmissionEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/password/ResetPasswordSubmissionEndpoint.java
index 4b3edeef10..d2ef37f07f 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/password/ResetPasswordSubmissionEndpoint.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/password/ResetPasswordSubmissionEndpoint.java
@@ -50,7 +50,7 @@ public class ResetPasswordSubmissionEndpoint extends UserRequestHandler {
         Map<String, String> queryParams = new HashMap<>();
         queryParams.put(Parameters.CLIENT_ID, client.getClientId());
 
-        userService.resetPassword(user, getAuthenticatedUser(context))
+        userService.resetPassword(user, client, getAuthenticatedUser(context))
                 .subscribe(
                         () -> {
                             queryParams.put("success", "reset_password_completed");
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/register/RegisterConfirmationEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/register/RegisterConfirmationEndpoint.java
index 93aff3ea7c..985ed4f71b 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/register/RegisterConfirmationEndpoint.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/endpoint/user/register/RegisterConfirmationEndpoint.java
@@ -41,6 +41,7 @@ public class RegisterConfirmationEndpoint extends UserRequestHandler {
     private static final Logger logger = LoggerFactory.getLogger(RegisterConfirmationEndpoint.class);
     private static final String ERROR_PARAM = "error";
     private static final String SUCCESS_PARAM = "success";
+    private static final String WARNING_PARAM = "warning";
     private static final String TOKEN_PARAM = "token";
     private ThymeleafTemplateEngine engine;
 
@@ -53,10 +54,12 @@ public class RegisterConfirmationEndpoint extends UserRequestHandler {
         final HttpServerRequest request = routingContext.request();
         final String error = request.getParam(ERROR_PARAM);
         final String success = request.getParam(SUCCESS_PARAM);
+        final String warning = request.getParam(WARNING_PARAM);
         final String token = request.getParam(TOKEN_PARAM);
         // add query params to context
         routingContext.put(ERROR_PARAM, error);
         routingContext.put(SUCCESS_PARAM, success);
+        routingContext.put(WARNING_PARAM, warning);
         routingContext.put(TOKEN_PARAM, token);
 
         // retrieve user who want to register
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/user/UserService.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/user/UserService.java
index 945368a71c..f0e567fc81 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/user/UserService.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/user/UserService.java
@@ -36,7 +36,7 @@ public interface UserService {
 
     Completable confirmRegistration(User user, io.gravitee.am.identityprovider.api.User principal);
 
-    Completable resetPassword(User user, io.gravitee.am.identityprovider.api.User principal);
+    Completable resetPassword(User user, Client client, io.gravitee.am.identityprovider.api.User principal);
 
     Completable forgotPassword(String email, Client client, io.gravitee.am.identityprovider.api.User principal);
 
@@ -44,8 +44,8 @@ public interface UserService {
         return register(user, null);
     }
 
-    default Completable resetPassword(User user) {
-        return resetPassword(user, null);
+    default Completable resetPassword(User user, Client client) {
+        return resetPassword(user, client, null);
     }
 
     default Completable forgotPassword(String email, Client client) {
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/user/impl/UserServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/user/impl/UserServiceImpl.java
index 5da6d70fa2..81d85539ad 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/user/impl/UserServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/user/impl/UserServiceImpl.java
@@ -34,6 +34,7 @@ import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
 import io.gravitee.am.model.Template;
 import io.gravitee.am.model.User;
+import io.gravitee.am.model.account.AccountSettings;
 import io.gravitee.am.repository.management.api.UserRepository;
 import io.gravitee.am.repository.management.api.search.LoginAttemptCriteria;
 import io.gravitee.am.service.AuditService;
@@ -41,6 +42,7 @@ import io.gravitee.am.service.LoginAttemptService;
 import io.gravitee.am.service.exception.UserAlreadyExistsException;
 import io.gravitee.am.service.exception.UserNotFoundException;
 import io.gravitee.am.service.exception.UserProviderNotFoundException;
+import io.gravitee.am.service.exception.authentication.AccountInactiveException;
 import io.gravitee.am.service.reporter.builder.AuditBuilder;
 import io.gravitee.am.service.reporter.builder.management.UserAuditBuilder;
 import io.reactivex.Completable;
@@ -185,13 +187,32 @@ public class UserServiceImpl implements UserService {
     }
 
     @Override
-    public Completable resetPassword(User user, io.gravitee.am.identityprovider.api.User principal) {
+    public Completable resetPassword(User user, Client client, io.gravitee.am.identityprovider.api.User principal) {
+        // if user registration is not completed and force registration option is disabled throw invalid account exception
+        if (user.isInactive() && !forceUserRegistration(domain, client)) {
+            return Completable.error(new AccountInactiveException("User needs to complete the activation process"));
+        }
+
         // only idp manage password, find user idp and update its password
         return identityProviderManager.getUserProvider(user.getSource())
                 .switchIfEmpty(Maybe.error(new UserProviderNotFoundException(user.getSource())))
-                .flatMapSingle(userProvider -> userProvider.update(user.getExternalId(), convert(user)))
+                // update the idp user
+                .flatMapSingle(userProvider ->
+                        userProvider.update(user.getExternalId(), convert(user))
+                                .onErrorResumeNext(ex -> {
+                                    if (ex instanceof UserNotFoundException) {
+                                        // idp user not found, create its account
+                                        return userProvider.create(convert(user));
+                                    }
+                                    return Single.error(ex);
+                                }))
+                // update the user in the AM repository
                 .flatMap(idpUser -> {
                     // update 'users' collection for management and audit purpose
+                    // if user was in pre-registration mode, end the registration process
+                    if (user.isPreRegistration()) {
+                        user.setRegistrationCompleted(true);
+                    }
                     user.setPassword(null);
                     user.setExternalId(idpUser.getId());
                     user.setUpdatedAt(new Date());
@@ -216,13 +237,19 @@ public class UserServiceImpl implements UserService {
                 .map(users -> users.stream().filter(user -> user.isInternal()).findFirst())
                 .flatMapMaybe(optionalUser -> optionalUser.isPresent() ? Maybe.just(optionalUser.get()) : Maybe.empty())
                 .switchIfEmpty(Maybe.error(new UserNotFoundException(email)))
-                .toSingle()
+                .map(user -> {
+                    // if user registration is not completed and force registration option is disabled throw invalid account exception
+                    if (user.isInactive() && !forceUserRegistration(domain, client)) {
+                        throw new AccountInactiveException("User needs to complete the activation process");
+                    }
+                    return user;
+                })
                 .doOnSuccess(user -> new Thread(() -> completeForgotPassword(user, client)).start())
                 .doOnSuccess(user1 -> {
                     // reload principal
                     io.gravitee.am.identityprovider.api.User principal1 = new DefaultUser(user1.getUsername());
                     ((DefaultUser) principal1).setId(user1.getId());
-                    ((DefaultUser) principal1).setAdditionalInformation(principal.getAdditionalInformation());
+                    ((DefaultUser) principal1).setAdditionalInformation(principal != null && principal.getAdditionalInformation() != null ? new HashMap<>(principal.getAdditionalInformation()) : new HashMap<>());
                     principal1.getAdditionalInformation()
                             .put(StandardClaims.NAME,
                                     user1.getDisplayName() != null ? user1.getDisplayName() :
@@ -230,7 +257,7 @@ public class UserServiceImpl implements UserService {
                     auditService.report(AuditBuilder.builder(UserAuditBuilder.class).domain(domain.getId()).client(client).principal(principal1).type(EventType.FORGOT_PASSWORD_REQUESTED));
                 })
                 .doOnError(throwable -> auditService.report(AuditBuilder.builder(UserAuditBuilder.class).domain(domain.getId()).client(client).principal(principal).type(EventType.FORGOT_PASSWORD_REQUESTED).throwable(throwable)))
-                .toCompletable();
+                .toSingle().toCompletable();
 
     }
 
@@ -297,6 +324,28 @@ public class UserServiceImpl implements UserService {
                 .defaultIfEmpty(Optional.empty());
     }
 
+    private boolean forceUserRegistration(Domain domain, Client client) {
+        AccountSettings accountSettings = getAccountSettings(domain, client);
+        return accountSettings != null && accountSettings.isCompleteRegistrationWhenResetPassword();
+    }
+
+    private AccountSettings getAccountSettings(Domain domain, Client client) {
+        // if client has no account config return domain config
+        if (client != null) {
+            if (client.getAccountSettings() == null) {
+                return domain.getAccountSettings();
+            }
+
+            // if client configuration is not inherited return the client config
+            if (!client.getAccountSettings().isInherited()) {
+                return client.getAccountSettings();
+            }
+        }
+
+        // return domain config
+        return domain.getAccountSettings();
+    }
+
     private io.gravitee.am.identityprovider.api.User convert(User user) {
         DefaultUser idpUser = new DefaultUser(user.getUsername());
         idpUser.setCredentials(user.getPassword());
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/scope/ScopeService.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/scope/ScopeService.java
index 8e059e518d..a29325c962 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/scope/ScopeService.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/scope/ScopeService.java
@@ -18,6 +18,7 @@ package io.gravitee.am.gateway.handler.oauth2.service.scope;
 import io.gravitee.am.model.oauth2.Scope;
 import io.reactivex.Single;
 
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -27,4 +28,6 @@ import java.util.Set;
 public interface ScopeService {
 
     Single<Set<Scope>> getAll();
+
+    List<String> getDiscoveryScope();
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/scope/impl/ScopeServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/scope/impl/ScopeServiceImpl.java
index 4a8173661f..9944183511 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/scope/impl/ScopeServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/scope/impl/ScopeServiceImpl.java
@@ -21,7 +21,10 @@ import io.gravitee.am.model.oauth2.Scope;
 import io.reactivex.Single;
 import org.springframework.beans.factory.annotation.Autowired;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
@@ -36,4 +39,14 @@ public class ScopeServiceImpl implements ScopeService {
     public Single<Set<Scope>> getAll() {
         return Single.just(scopeManager.findAll());
     }
+
+    @Override
+    public List<String> getDiscoveryScope() {
+        return scopeManager.findAll()
+                .stream()
+                .filter(scope -> scope.isDiscovery())
+                .map(Scope::getKey)
+                .sorted()
+                .collect(Collectors.toList());
+    }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java
index 59fc6b8034..26e7fa5ad1 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java
@@ -16,12 +16,11 @@
 package io.gravitee.am.gateway.handler.oidc.service.discovery.impl;
 
 import io.gravitee.am.common.oauth2.CodeChallengeMethod;
-import io.gravitee.am.common.oauth2.GrantType;
-import io.gravitee.am.common.oauth2.ResponseType;
 import io.gravitee.am.common.oidc.ClaimType;
 import io.gravitee.am.common.oidc.ClientAuthenticationMethod;
 import io.gravitee.am.common.oidc.Scope;
 import io.gravitee.am.gateway.handler.common.jwa.utils.JWAlgorithmUtils;
+import io.gravitee.am.gateway.handler.oauth2.service.scope.ScopeService;
 import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDDiscoveryService;
 import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata;
 import io.gravitee.am.gateway.handler.oidc.service.utils.SubjectTypeUtils;
@@ -55,6 +54,9 @@ public class OpenIDDiscoveryServiceImpl implements OpenIDDiscoveryService {
     @Autowired
     private Domain domain;
 
+    @Autowired
+    private ScopeService scopeService;
+
     @Override
     public OpenIDProviderMetadata getConfiguration(String basePath) {
         OpenIDProviderMetadata openIDProviderMetadata = new OpenIDProviderMetadata();
@@ -73,7 +75,7 @@ public class OpenIDDiscoveryServiceImpl implements OpenIDDiscoveryService {
         openIDProviderMetadata.setRegistrationEndpoint(getEndpointAbsoluteURL(basePath, REGISTRATION_ENDPOINT));
 
         // supported parameters
-        openIDProviderMetadata.setScopesSupported(Stream.of(Scope.values()).map(Scope::getKey).collect(Collectors.toList()));
+        openIDProviderMetadata.setScopesSupported(scopeService.getDiscoveryScope());
         openIDProviderMetadata.setResponseTypesSupported(ResponseTypeUtils.getSupportedResponseTypes());
         openIDProviderMetadata.setGrantTypesSupported(GrantTypeUtils.getSupportedGrantTypes());
         openIDProviderMetadata.setIdTokenSigningAlgValuesSupported(JWAlgorithmUtils.getSupportedIdTokenSigningAlg());
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/StatusEntity.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/StatusEntity.java
new file mode 100644
index 0000000000..6992afe486
--- /dev/null
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/StatusEntity.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.management.handlers.management.api.model;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class StatusEntity {
+
+    private boolean enabled;
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ScopeResource.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ScopeResource.java
index 4b9eebb478..a22c5bcd49 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ScopeResource.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ScopeResource.java
@@ -22,6 +22,7 @@ import io.gravitee.am.service.DomainService;
 import io.gravitee.am.service.ScopeService;
 import io.gravitee.am.service.exception.DomainNotFoundException;
 import io.gravitee.am.service.exception.ScopeNotFoundException;
+import io.gravitee.am.service.model.PatchScope;
 import io.gravitee.am.service.model.UpdateScope;
 import io.gravitee.common.http.MediaType;
 import io.reactivex.Maybe;
@@ -43,6 +44,7 @@ import javax.ws.rs.core.Response;
 /**
  * @author David BRASSELY (david.brassely at graviteesource.com)
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
  * @author GraviteeSource Team
  */
 public class ScopeResource extends AbstractResource {
@@ -81,12 +83,35 @@ public class ScopeResource extends AbstractResource {
                         error -> response.resume(error));
     }
 
+    @PATCH
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(value = "Patch a scope")
+    @ApiResponses({
+            @ApiResponse(code = 200, message = "Scope successfully patched", response = Scope.class),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void patch(
+            @PathParam("domain") String domain,
+            @PathParam("scope") String scope,
+            @ApiParam(name = "scope", required = true) @Valid @NotNull PatchScope patchScope,
+            @Suspended final AsyncResponse response) {
+        final User authenticatedUser = getAuthenticatedUser();
+
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMapSingle(irrelevant -> scopeService.patch(domain, scope, patchScope, authenticatedUser))
+                .map(scope1 -> Response.ok(scope1).build())
+                .subscribe(
+                        result -> response.resume(result),
+                        error -> response.resume(error));
+    }
+
     @PUT
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     @ApiOperation(value = "Update a scope")
     @ApiResponses({
-            @ApiResponse(code = 201, message = "Scope successfully updated", response = Client.class),
+            @ApiResponse(code = 200, message = "Scope successfully updated", response = Scope.class),
             @ApiResponse(code = 500, message = "Internal server error")})
     public void update(
             @PathParam("domain") String domain,
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java
index c7b5237d14..309253ebb9 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java
@@ -17,6 +17,7 @@ package io.gravitee.am.management.handlers.management.api.resources;
 
 import io.gravitee.am.management.handlers.management.api.model.ClientEntity;
 import io.gravitee.am.management.handlers.management.api.model.PasswordValue;
+import io.gravitee.am.management.handlers.management.api.model.StatusEntity;
 import io.gravitee.am.management.handlers.management.api.model.UserEntity;
 import io.gravitee.am.management.service.UserService;
 import io.gravitee.am.model.User;
@@ -120,6 +121,30 @@ public class UserResource extends AbstractResource {
                         error -> response.resume(error));
     }
 
+    @PUT
+    @Path("/status")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(value = "Update a user status")
+    @ApiResponses({
+            @ApiResponse(code = 201, message = "User status successfully updated", response = User.class),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void updateUserStatus(
+            @PathParam("domain") String domain,
+            @PathParam("user") String user,
+            @ApiParam(name = "status", required = true) @Valid @NotNull StatusEntity status,
+            @Suspended final AsyncResponse response) {
+        final io.gravitee.am.identityprovider.api.User authenticatedUser = getAuthenticatedUser();
+
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMapSingle(irrelevant -> userService.updateStatus(domain, user, status.isEnabled(), authenticatedUser))
+                .map(user1 -> Response.ok(user1).build())
+                .subscribe(
+                        result -> response.resume(result),
+                        error -> response.resume(error));
+    }
+
     @DELETE
     @ApiOperation(value = "Delete a user")
     @ApiResponses({
diff --git a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/UserService.java b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/UserService.java
index ac824930ed..55b88a183e 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/UserService.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/UserService.java
@@ -39,6 +39,8 @@ public interface UserService {
 
     Single<User> update(String domain, String id, UpdateUser updateUser, io.gravitee.am.identityprovider.api.User principal);
 
+    Single<User> updateStatus(String domain, String id, boolean status, io.gravitee.am.identityprovider.api.User principal);
+
     Completable delete(String userId, io.gravitee.am.identityprovider.api.User principal);
 
     Completable resetPassword(String domain, String userId, String password, io.gravitee.am.identityprovider.api.User principal);
@@ -55,6 +57,10 @@ public interface UserService {
         return update(domain, id, updateUser, null);
     }
 
+    default Single<User> updateStatus(String domain, String userId, boolean status) {
+        return updateStatus(domain, userId, status, null);
+    }
+
     default Completable delete(String userId) {
         return delete(userId, null);
     }
diff --git a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java
index 4841432684..cf22119a49 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java
@@ -208,6 +208,18 @@ public class UserServiceImpl implements UserService {
                 );
     }
 
+    @Override
+    public Single<User> updateStatus(String domain, String id, boolean status, io.gravitee.am.identityprovider.api.User principal) {
+        return userService.findById(id)
+                .switchIfEmpty(Maybe.error(new UserNotFoundException(id)))
+                .flatMapSingle(user -> {
+                    user.setEnabled(status);
+                    return userService.update(user);
+                })
+                .doOnSuccess(user1 -> auditService.report(AuditBuilder.builder(UserAuditBuilder.class).principal(principal).type((status ? EventType.USER_ENABLED : EventType.USER_DISABLED)).user(user1)))
+                .doOnError(throwable -> auditService.report(AuditBuilder.builder(UserAuditBuilder.class).principal(principal).type(EventType.USER_UPDATED).throwable(throwable)));
+    }
+
     @Override
     public Completable delete(String userId, io.gravitee.am.identityprovider.api.User principal) {
         return userService.findById(userId)
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 70339f2d1b..bbcb34fd4e 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
@@ -67,9 +67,10 @@ public class OpenIDScopeUpgrader implements Upgrader, Ordered {
     }
 
     private Single<Scope> createSystemScope(String domain, io.gravitee.am.common.oidc.Scope systemScope) {
-        return scopeService.findByDomain(domain)
-                .flatMap(scopes -> {
-                    Optional<Scope> optScope = scopes.stream().filter(scope -> scope.getKey().equalsIgnoreCase(systemScope.getKey())).findFirst();
+        return scopeService.findByDomainAndKey(domain, systemScope.getKey())
+                .map(scope -> Optional.of(scope))
+                .defaultIfEmpty(Optional.empty())
+                .flatMapSingle(optScope -> {
                     if (!optScope.isPresent()) {
                         logger.info("Create a new system scope key[{}] for domain[{}]", systemScope.getKey(), domain);
                         NewSystemScope scope = new NewSystemScope();
@@ -77,8 +78,9 @@ public class OpenIDScopeUpgrader implements Upgrader, Ordered {
                         scope.setClaims(systemScope.getClaims());
                         scope.setName(systemScope.getLabel());
                         scope.setDescription(systemScope.getDescription());
+                        scope.setDiscovery(systemScope.isDiscovery());
                         return scopeService.create(domain, scope);
-                    } else if (!optScope.get().isSystem()){
+                    } else if (shouldUpdateSystemScope(optScope, systemScope)){
                         logger.info("Update a system scope key[{}] for domain[{}]", systemScope.getKey(), domain);
                         final Scope existingScope = optScope.get();
                         UpdateSystemScope scope = new UpdateSystemScope();
@@ -86,11 +88,24 @@ public class OpenIDScopeUpgrader implements Upgrader, Ordered {
                         scope.setDescription(existingScope.getDescription() != null ? existingScope.getDescription() : systemScope.getDescription());
                         scope.setClaims(systemScope.getClaims());
                         scope.setExpiresIn(existingScope.getExpiresIn());
+                        scope.setDiscovery(systemScope.isDiscovery());
                         return scopeService.update(domain, optScope.get().getId(), scope);
                     }
                     return Single.just(optScope.get());
                 });
     }
+
+    /**
+     * Update System scope if it is not currently set as system or if discovery property does not match.
+     * @param optScope
+     * @param systemScope
+     * @return
+     */
+    private boolean shouldUpdateSystemScope(Optional<Scope> optScope, io.gravitee.am.common.oidc.Scope systemScope) {
+        //If not currently set as system or if discovery property does not match
+        return !optScope.get().isSystem() || optScope.get().isDiscovery() != systemScope.isDiscovery();
+    }
+
     @Override
     public int getOrder() {
         return 162;
diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/User.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/User.java
index 87c365bdf6..57461f6897 100644
--- a/gravitee-am-model/src/main/java/io/gravitee/am/model/User.java
+++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/User.java
@@ -410,4 +410,8 @@ public class User {
     public void setUpdatedAt(Date updatedAt) {
         this.updatedAt = updatedAt;
     }
+
+    public boolean isInactive() {
+        return isPreRegistration() && !isRegistrationCompleted();
+    }
 }
diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/account/AccountSettings.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/account/AccountSettings.java
index ad6f983d4f..9f061045f4 100644
--- a/gravitee-am-model/src/main/java/io/gravitee/am/model/account/AccountSettings.java
+++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/account/AccountSettings.java
@@ -43,6 +43,11 @@ public class AccountSettings {
      */
     private Integer accountBlockedDuration;
 
+    /**
+     * Complete user registration when the user renewed his password
+     */
+    private boolean completeRegistrationWhenResetPassword;
+
     public boolean isInherited() {
         return inherited;
     }
@@ -82,4 +87,12 @@ public class AccountSettings {
     public void setAccountBlockedDuration(Integer accountBlockedDuration) {
         this.accountBlockedDuration = accountBlockedDuration;
     }
+
+    public boolean isCompleteRegistrationWhenResetPassword() {
+        return completeRegistrationWhenResetPassword;
+    }
+
+    public void setCompleteRegistrationWhenResetPassword(boolean completeRegistrationWhenResetPassword) {
+        this.completeRegistrationWhenResetPassword = completeRegistrationWhenResetPassword;
+    }
 }
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 0703e23efe..0c94ab061e 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
@@ -63,6 +63,8 @@ public class Scope {
      */
     private Integer expiresIn;
 
+    private boolean discovery;
+
     public Scope() {
     }
 
@@ -81,6 +83,7 @@ public class Scope {
         this.system = other.system;
         this.claims = other.claims;
         this.expiresIn = other.expiresIn;
+        this.discovery = other.discovery;
     }
 
     public String getId() {
@@ -163,6 +166,14 @@ public class Scope {
         this.expiresIn = expiresIn;
     }
 
+    public boolean isDiscovery() {
+        return discovery;
+    }
+
+    public void setDiscovery(boolean discovery) {
+        this.discovery = discovery;
+    }
+
     @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 f8a380e353..c5b8aa6d57 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
@@ -448,6 +448,7 @@ public class MongoClientRepository extends AbstractManagementMongoRepository imp
         accountSettings.setMaxLoginAttempts(accountSettingsMongo.getMaxLoginAttempts());
         accountSettings.setLoginAttemptsResetTime(accountSettingsMongo.getLoginAttemptsResetTime());
         accountSettings.setAccountBlockedDuration(accountSettingsMongo.getAccountBlockedDuration());
+        accountSettings.setCompleteRegistrationWhenResetPassword(accountSettingsMongo.isCompleteRegistrationWhenResetPassword());
         return accountSettings;
     }
 
@@ -462,6 +463,7 @@ public class MongoClientRepository extends AbstractManagementMongoRepository imp
         accountSettingsMongo.setMaxLoginAttempts(accountSettings.getMaxLoginAttempts());
         accountSettingsMongo.setLoginAttemptsResetTime(accountSettings.getLoginAttemptsResetTime());
         accountSettingsMongo.setAccountBlockedDuration(accountSettings.getAccountBlockedDuration());
+        accountSettingsMongo.setCompleteRegistrationWhenResetPassword(accountSettings.isCompleteRegistrationWhenResetPassword());
         return accountSettingsMongo;
     }
 }
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java
index 69c7eee7f1..229f8adf18 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java
@@ -311,6 +311,7 @@ public class MongoDomainRepository extends AbstractManagementMongoRepository imp
         accountSettings.setMaxLoginAttempts(accountSettingsMongo.getMaxLoginAttempts());
         accountSettings.setLoginAttemptsResetTime(accountSettingsMongo.getLoginAttemptsResetTime());
         accountSettings.setAccountBlockedDuration(accountSettingsMongo.getAccountBlockedDuration());
+        accountSettings.setCompleteRegistrationWhenResetPassword(accountSettingsMongo.isCompleteRegistrationWhenResetPassword());
         return accountSettings;
     }
 
@@ -325,6 +326,7 @@ public class MongoDomainRepository extends AbstractManagementMongoRepository imp
         accountSettingsMongo.setMaxLoginAttempts(accountSettings.getMaxLoginAttempts());
         accountSettingsMongo.setLoginAttemptsResetTime(accountSettings.getLoginAttemptsResetTime());
         accountSettingsMongo.setAccountBlockedDuration(accountSettings.getAccountBlockedDuration());
+        accountSettingsMongo.setCompleteRegistrationWhenResetPassword(accountSettings.isCompleteRegistrationWhenResetPassword());
         return accountSettingsMongo;
     }
 
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 1108000cd9..35e8137908 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
@@ -104,6 +104,7 @@ public class MongoScopeRepository extends AbstractManagementMongoRepository impl
         scope.setExpiresIn(scopeMongo.getExpiresIn());
         scope.setCreatedAt(scopeMongo.getCreatedAt());
         scope.setUpdatedAt(scopeMongo.getUpdatedAt());
+        scope.setDiscovery(scopeMongo.isDiscovery());
 
         return scope;
     }
@@ -124,6 +125,7 @@ public class MongoScopeRepository extends AbstractManagementMongoRepository impl
         scopeMongo.setExpiresIn(scope.getExpiresIn());
         scopeMongo.setCreatedAt(scope.getCreatedAt());
         scopeMongo.setUpdatedAt(scope.getUpdatedAt());
+        scopeMongo.setDiscovery(scope.isDiscovery());
 
         return scopeMongo;
     }
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/AccountSettingsMongo.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/AccountSettingsMongo.java
index 83416ad2f5..b9d6cf0409 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/AccountSettingsMongo.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/AccountSettingsMongo.java
@@ -26,6 +26,7 @@ public class AccountSettingsMongo {
     private Integer maxLoginAttempts;
     private Integer loginAttemptsResetTime;
     private Integer accountBlockedDuration;
+    private boolean completeRegistrationWhenResetPassword;
 
     public boolean isInherited() {
         return inherited;
@@ -66,4 +67,12 @@ public class AccountSettingsMongo {
     public void setAccountBlockedDuration(Integer accountBlockedDuration) {
         this.accountBlockedDuration = accountBlockedDuration;
     }
+
+    public boolean isCompleteRegistrationWhenResetPassword() {
+        return completeRegistrationWhenResetPassword;
+    }
+
+    public void setCompleteRegistrationWhenResetPassword(boolean completeRegistrationWhenResetPassword) {
+        this.completeRegistrationWhenResetPassword = completeRegistrationWhenResetPassword;
+    }
 }
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 474f4cef81..1d9879731a 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
@@ -53,6 +53,8 @@ public class ScopeMongo extends Auditable {
 
     private Integer expiresIn;
 
+    private boolean discovery;
+
     public String getId() {
         return id;
     }
@@ -117,6 +119,14 @@ public class ScopeMongo extends Auditable {
         this.expiresIn = expiresIn;
     }
 
+    public boolean isDiscovery() {
+        return discovery;
+    }
+
+    public void setDiscovery(boolean discovery) {
+        this.discovery = discovery;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeService.java
index ca988488e4..e3c7c05939 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/ScopeService.java
@@ -19,6 +19,7 @@ import io.gravitee.am.identityprovider.api.User;
 import io.gravitee.am.model.oauth2.Scope;
 import io.gravitee.am.service.model.NewScope;
 import io.gravitee.am.service.model.NewSystemScope;
+import io.gravitee.am.service.model.PatchScope;
 import io.gravitee.am.service.model.UpdateScope;
 import io.gravitee.am.service.model.UpdateSystemScope;
 import io.reactivex.Completable;
@@ -47,6 +48,8 @@ public interface ScopeService {
 
     Maybe<Scope> findByDomainAndKey(String domain, String scopeKey);
 
+    Single<Scope> patch(String domain, String id, PatchScope patchScope, User principal);
+
     Single<Scope> update(String domain, String id, UpdateScope updateScope, User principal);
 
     Single<Scope> update(String domain, String id, UpdateSystemScope updateScope);
@@ -63,6 +66,10 @@ public interface ScopeService {
         return create(domain, scope, null);
     }
 
+    default Single<Scope> patch(String domain, String id, PatchScope patchScope) {
+        return patch(domain, id, patchScope, null);
+    }
+
     default Single<Scope> update(String domain, String id, UpdateScope updateScope) {
         return update(domain, id, updateScope, null);
     }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountDisabledException.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountDisabledException.java
index 94abf2d5d9..d9a3ffb3b7 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountDisabledException.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountDisabledException.java
@@ -19,14 +19,7 @@ package io.gravitee.am.service.exception.authentication;
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
  * @author GraviteeSource Team
  */
-public class AccountDisabledException extends AuthenticationException {
-
-    public AccountDisabledException() {
-    }
-
-    public AccountDisabledException(String msg, Throwable t) {
-        super(msg, t);
-    }
+public class AccountDisabledException extends AccountStatusException {
 
     public AccountDisabledException(String msg) {
         super(msg);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountInactiveException.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountInactiveException.java
new file mode 100644
index 0000000000..71e07d8264
--- /dev/null
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountInactiveException.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.service.exception.authentication;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class AccountInactiveException extends AccountStatusException {
+
+    public AccountInactiveException(String msg) {
+        super(msg);
+    }
+
+    @Override
+    public String getErrorCode() {
+        return "account_inactive";
+    }
+}
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountLockedException.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountLockedException.java
index 6c4b34494c..cf4a5f2c37 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountLockedException.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountLockedException.java
@@ -21,33 +21,14 @@ import java.util.Map;
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
  * @author GraviteeSource Team
  */
-public class AccountLockedException extends AuthenticationException {
+public class AccountLockedException extends AccountStatusException {
 
     public static final String ERROR_CODE = "account_locked";
 
-    public AccountLockedException() {
-    }
-
-    public AccountLockedException(String msg, Throwable t) {
-        super(msg, t);
-    }
-
-    public AccountLockedException(String msg) {
-        super(msg);
-    }
-
-    public AccountLockedException(Map<String, String> details) {
-        super(details);
-    }
-
     public AccountLockedException(String msg, Map<String, String> details) {
         super(msg, details);
     }
 
-    public AccountLockedException(String msg, Throwable throwable, Map<String, String> details) {
-        super(msg, throwable, details);
-    }
-
     @Override
     public String getErrorCode() {
         return ERROR_CODE;
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountStatusException.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountStatusException.java
new file mode 100644
index 0000000000..1c81d08cf8
--- /dev/null
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/exception/authentication/AccountStatusException.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.service.exception.authentication;
+
+import java.util.Map;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public abstract class AccountStatusException extends AuthenticationException {
+
+    public AccountStatusException(String msg) {
+        super(msg);
+    }
+
+    public AccountStatusException(String msg, Map<String, String> details) {
+        super(msg, details);
+    }
+}
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 663269fdd4..3bd5d8dfbf 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
@@ -223,7 +223,7 @@ public class ClientServiceImpl implements ClientService {
         LOGGER.debug("Find top clients");
         return clientRepository.findAll()
                 .flatMapObservable(clients -> Observable.fromIterable(clients))
-                .flatMapSingle(client -> accessTokenRepository.countByClientId(client.getId())
+                .flatMapSingle(client -> accessTokenRepository.countByClientId(client.getClientId())
                         .map(oAuth2AccessTokens -> {
                             TopClient topClient = new TopClient();
                             topClient.setClient(client);
@@ -243,7 +243,7 @@ public class ClientServiceImpl implements ClientService {
         LOGGER.debug("Find top clients by domain: {}", domain);
         return clientRepository.findByDomain(domain)
                 .flatMapObservable(clients -> Observable.fromIterable(clients))
-                .flatMapSingle(client -> accessTokenRepository.countByClientId(client.getId())
+                .flatMapSingle(client -> accessTokenRepository.countByClientId(client.getClientId())
                         .map(oAuth2AccessTokens -> {
                             TopClient topClient = new TopClient();
                             topClient.setClient(client);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/DomainServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/DomainServiceImpl.java
index dac284e7c0..86a39d0f1b 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/DomainServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/DomainServiceImpl.java
@@ -430,6 +430,7 @@ public class DomainServiceImpl implements DomainService {
                     scope.setClaims(systemScope.getClaims());
                     scope.setName(systemScope.getLabel());
                     scope.setDescription(systemScope.getDescription());
+                    scope.setDiscovery(systemScope.isDiscovery());
                     return scopeService.create(domain.getId(), scope);
                 })
                 .lastOrError()
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 72358c1a7b..ee85a1b1f4 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
@@ -106,6 +106,7 @@ public class ScopeServiceImpl implements ScopeService {
                     scope.setName(newScope.getName());
                     scope.setDescription(newScope.getDescription());
                     scope.setExpiresIn(newScope.getExpiresIn());
+                    scope.setDiscovery(newScope.isDiscovery());
                     scope.setCreatedAt(new Date());
                     scope.setUpdatedAt(new Date());
 
@@ -147,6 +148,7 @@ public class ScopeServiceImpl implements ScopeService {
                     scope.setName(newScope.getName());
                     scope.setDescription(newScope.getDescription());
                     scope.setExpiresIn(newScope.getExpiresIn());
+                    scope.setDiscovery(newScope.isDiscovery());
                     scope.setCreatedAt(new Date());
                     scope.setUpdatedAt(new Date());
                     return scopeRepository.create(scope);
@@ -165,6 +167,24 @@ public class ScopeServiceImpl implements ScopeService {
                 });
     }
 
+    @Override
+    public Single<Scope> patch(String domain, String id, PatchScope patchScope, User principal) {
+        LOGGER.debug("Patching a scope {} for domain {}", id, domain);
+        return scopeRepository.findById(id)
+                .switchIfEmpty(Maybe.error(new ScopeNotFoundException(id)))
+                .flatMapSingle(oldScope -> {
+                    Scope scopeToUpdate = patchScope.patch(oldScope);
+                    return update(domain, scopeToUpdate, oldScope, principal);
+                })
+                .onErrorResumeNext(ex -> {
+                    if (ex instanceof AbstractManagementException) {
+                        return Single.error(ex);
+                    }
+                    LOGGER.error("An error occurs while trying to patch a scope", ex);
+                    return Single.error(new TechnicalManagementException("An error occurs while trying to patch a scope", ex));
+                });
+    }
+
     @Override
     public Single<Scope> update(String domain, String id, UpdateScope updateScope, User principal) {
         LOGGER.debug("Update a scope {} for domain {}", id, domain);
@@ -175,16 +195,11 @@ public class ScopeServiceImpl implements ScopeService {
                     scopeToUpdate.setName(updateScope.getName());
                     scopeToUpdate.setDescription(updateScope.getDescription());
                     scopeToUpdate.setExpiresIn(updateScope.getExpiresIn());
-                    scopeToUpdate.setUpdatedAt(new Date());
-
-                    return scopeRepository.update(scopeToUpdate)
-                            .flatMap(scope1 -> {
-                                // Reload domain to take care about scope update
-                                Event event = new Event(Type.SCOPE, new Payload(scope1.getId(), scope1.getDomain(), Action.UPDATE));
-                                return domainService.reload(domain, event).flatMap(domain1 -> Single.just(scope1));
-                            })
-                            .doOnSuccess(scope1 -> auditService.report(AuditBuilder.builder(ScopeAuditBuilder.class).principal(principal).type(EventType.SCOPE_UPDATED).oldValue(oldScope).scope(scope1)))
-                            .doOnError(throwable -> auditService.report(AuditBuilder.builder(ScopeAuditBuilder.class).principal(principal).type(EventType.SCOPE_UPDATED).throwable(throwable)));
+                    if(!oldScope.isSystem() && updateScope.getDiscovery()!=null) {
+                        scopeToUpdate.setDiscovery(updateScope.isDiscovery());
+                    }
+
+                    return update(domain, scopeToUpdate, oldScope, principal);
                 })
                 .onErrorResumeNext(ex -> {
                     if (ex instanceof AbstractManagementException) {
@@ -195,6 +210,19 @@ public class ScopeServiceImpl implements ScopeService {
                 });
     }
 
+    private Single<Scope> update(String domain, Scope toUpdate, Scope oldValue, User principal) {
+
+        toUpdate.setUpdatedAt(new Date());
+        return scopeRepository.update(toUpdate)
+                .flatMap(scope1 -> {
+                    // Reload domain to take care about scope update
+                    Event event = new Event(Type.SCOPE, new Payload(scope1.getId(), scope1.getDomain(), Action.UPDATE));
+                    return domainService.reload(domain, event).flatMap(domain1 -> Single.just(scope1));
+                })
+                .doOnSuccess(scope1 -> auditService.report(AuditBuilder.builder(ScopeAuditBuilder.class).principal(principal).type(EventType.SCOPE_UPDATED).oldValue(oldValue).scope(scope1)))
+                .doOnError(throwable -> auditService.report(AuditBuilder.builder(ScopeAuditBuilder.class).principal(principal).type(EventType.SCOPE_UPDATED).throwable(throwable)));
+    }
+
     @Override
     public Single<Scope> update(String domain, String id, UpdateSystemScope updateScope) {
         LOGGER.debug("Update a system scope {} for domain {}", id, domain);
@@ -207,6 +235,7 @@ public class ScopeServiceImpl implements ScopeService {
                     scope.setSystem(true);
                     scope.setClaims(updateScope.getClaims());
                     scope.setExpiresIn(updateScope.getExpiresIn());
+                    scope.setDiscovery(updateScope.isDiscovery());
                     return scopeRepository.update(scope);
                 })
                 .flatMap(scope -> {
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 53b2ff54a6..59327edb8f 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
@@ -34,6 +34,8 @@ public class NewScope {
 
     private Integer expiresIn;
 
+    private boolean discovery;
+
     public String getKey() {
         return key;
     }
@@ -65,4 +67,12 @@ public class NewScope {
     public void setExpiresIn(Integer expiresIn) {
         this.expiresIn = expiresIn;
     }
+
+    public boolean isDiscovery() {
+        return discovery;
+    }
+
+    public void setDiscovery(boolean discovery) {
+        this.discovery = discovery;
+    }
 }
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 c7d5f44099..32b34e1939 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
@@ -37,6 +37,8 @@ public class NewSystemScope {
 
     private Integer expiresIn;
 
+    private boolean discovery;
+
     public String getKey() {
         return key;
     }
@@ -76,4 +78,12 @@ public class NewSystemScope {
     public void setExpiresIn(Integer expiresIn) {
         this.expiresIn = expiresIn;
     }
+
+    public boolean isDiscovery() {
+        return discovery;
+    }
+
+    public void setDiscovery(boolean discovery) {
+        this.discovery = discovery;
+    }
 }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchScope.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchScope.java
new file mode 100644
index 0000000000..91d8ece1d7
--- /dev/null
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchScope.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.service.model;
+
+import io.gravitee.am.model.oauth2.Scope;
+import io.gravitee.am.service.utils.SetterUtils;
+
+import java.util.Optional;
+
+/**
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
+ * @author GraviteeSource Team
+ */
+public class PatchScope {
+
+    private Optional<String> name;
+
+    private Optional<String> description;
+
+    private Optional<Integer> expiresIn;
+
+    private Optional<Boolean> discovery;
+
+    public Optional<String> getName() {
+        return name;
+    }
+
+    public void setName(Optional<String> name) {
+        this.name = name;
+    }
+
+    public Optional<String> getDescription() {
+        return description;
+    }
+
+    public void setDescription(Optional<String> description) {
+        this.description = description;
+    }
+
+    public Optional<Integer> getExpiresIn() {
+        return expiresIn;
+    }
+
+    public void setExpiresIn(Optional<Integer> expiresIn) {
+        this.expiresIn = expiresIn;
+    }
+
+    public Optional<Boolean> getDiscovery() {
+        return discovery;
+    }
+
+    public void setDiscovery(Optional<Boolean> discovery) {
+        this.discovery = discovery;
+    }
+
+    public Scope patch(Scope toPatch) {
+        Scope patched = new Scope(toPatch);
+        SetterUtils.safeSet(patched::setName,this.getName());
+        SetterUtils.safeSet(patched::setDescription, this.getDescription());
+        SetterUtils.safeSet(patched::setExpiresIn, this.getExpiresIn());
+        if(!toPatch.isSystem()){
+            SetterUtils.safeSet(patched::setDiscovery, this.getDiscovery(), boolean.class);
+        }
+        return patched;
+    }
+}
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 54e86d7db5..6a2e65ee1e 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
@@ -32,6 +32,8 @@ public class UpdateScope {
 
     private Integer expiresIn;
 
+    private Boolean discovery;
+
     public String getName() {
         return name;
     }
@@ -55,4 +57,16 @@ public class UpdateScope {
     public void setExpiresIn(Integer expiresIn) {
         this.expiresIn = expiresIn;
     }
+
+    public boolean isDiscovery() {
+        return discovery;
+    }
+
+    public void setDiscovery(boolean discovery) {
+        this.discovery = discovery;
+    }
+
+    public Boolean getDiscovery() {
+        return discovery;
+    }
 }
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 de08615535..97d975394e 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
@@ -34,6 +34,8 @@ public class UpdateSystemScope {
 
     private Integer expiresIn;
 
+    private boolean discovery;
+
     public String getName() {
         return name;
     }
@@ -65,4 +67,12 @@ public class UpdateSystemScope {
     public void setExpiresIn(Integer expiresIn) {
         this.expiresIn = expiresIn;
     }
+
+    public boolean isDiscovery() {
+        return discovery;
+    }
+
+    public void setDiscovery(boolean discovery) {
+        this.discovery = discovery;
+    }
 }
diff --git a/gravitee-am-ui/src/app/domain/clients/client/account/account.component.html b/gravitee-am-ui/src/app/domain/clients/client/account/account.component.html
index e3766215a2..afc80a0241 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/account/account.component.html
+++ b/gravitee-am-ui/src/app/domain/clients/client/account/account.component.html
@@ -18,7 +18,7 @@
 <div class="client-account-container">
   <div class="client-account-content">
     <div fxFlex="70">
-      <app-account-settings [accountSettings]="client.accountSettings" [inheritMode]="true" (onSavedAccountSettings)="updateAccountSettings($event)"></app-account-settings>
+      <app-account-settings [accountSettings]="accountSettings" [inheritMode]="true" (onSavedAccountSettings)="updateAccountSettings($event)"></app-account-settings>
     </div>
     <div class="client-account-description" fxFlex>
       <h3>User Accounts settings</h3>
diff --git a/gravitee-am-ui/src/app/domain/clients/client/account/account.component.scss b/gravitee-am-ui/src/app/domain/clients/client/account/account.component.scss
index ef58d6e0f3..cb2d43f2f8 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/account/account.component.scss
+++ b/gravitee-am-ui/src/app/domain/clients/client/account/account.component.scss
@@ -1,10 +1,8 @@
 .client-account-container {
-  padding: 0px 20px 0px 20px;
   h1 {
     font-weight: 400;
   }
   .client-account-content {
-    padding: 50px 0 20px;
     .client-account-description {
       margin-left: 20px;
       h3 {
diff --git a/gravitee-am-ui/src/app/domain/clients/client/account/account.component.ts b/gravitee-am-ui/src/app/domain/clients/client/account/account.component.ts
index 9b6111ad81..4f324599e6 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/account/account.component.ts
+++ b/gravitee-am-ui/src/app/domain/clients/client/account/account.component.ts
@@ -26,6 +26,7 @@ import {SnackbarService} from "../../../../services/snackbar.service";
 export class ClientAccountSettingsComponent {
   private domainId: string;
   client: any;
+  accountSettings: any;
 
   constructor(private route: ActivatedRoute,
               private clientService: ClientService,
@@ -35,11 +36,14 @@ export class ClientAccountSettingsComponent {
   ngOnInit() {
     this.domainId = this.route.snapshot.parent.parent.params['domainId'];
     this.client = this.route.snapshot.parent.data['client'];
+    this.accountSettings = Object.assign({}, this.client.accountSettings);
   }
 
   updateAccountSettings(accountSettings) {
+    this.accountSettings = accountSettings;
     this.clientService.patchAccountSettings(this.domainId, this.client.id, accountSettings).subscribe(data => {
       this.client = data;
+      this.route.snapshot.parent.data['client'] = this.client;
       this.snackbarService.open("User Accounts Settings updated");
     });
   }
diff --git a/gravitee-am-ui/src/app/domain/components/account/account-settings.component.html b/gravitee-am-ui/src/app/domain/components/account/account-settings.component.html
index fbb092f597..87a35187e9 100644
--- a/gravitee-am-ui/src/app/domain/components/account/account-settings.component.html
+++ b/gravitee-am-ui/src/app/domain/components/account/account-settings.component.html
@@ -27,54 +27,71 @@
     </div>
     <mat-divider></mat-divider>
   </div>
-  <div *ngIf="!localAccountSettings.inherited">
-    <div fxLayout="column" style="margin-top: 20px;">
-      <mat-slide-toggle
-        (change)="enableBrutForceAuthenticationDetection($event)"
-        [checked]="isBrutForceAuthenticationEnabled()">
-        Brute Force Detection
-      </mat-slide-toggle>
-      <mat-hint style="font-size: 75%;">Prevent brute force authentication attempts.</mat-hint>
-    </div>
-    <div fxLayout="row">
-      <mat-form-field>
-        <input matInput type="number" placeholder="Max Login Attempts" name="maxLoginAttempts" [(ngModel)]="localAccountSettings.maxLoginAttempts" [disabled]="!isBrutForceAuthenticationEnabled()" required>
-        <mat-hint>Max login failures after which the account will be blocked.</mat-hint>
-      </mat-form-field>
-    </div>
-    <div fxLayout="row">
-      <mat-form-field>
-        <input matInput type="number" placeholder="Login Attempts Reset Time" name="loginAttemptsResetTime" [(ngModel)]="localAccountSettings.loginAttemptsResetTime" [disabled]="!isBrutForceAuthenticationEnabled()" required>
-        <mat-hint>Time after which the login attempts will be reset if max attempts has not been reached.</mat-hint>
-      </mat-form-field>
-      <mat-form-field fxFlex style="margin-left: 20px;">
-        <mat-select placeholder="Unit time" name="loginAttemptsResetTimeUnitTime" [(ngModel)]="localAccountSettings.loginAttemptsResetTimeUnitTime" [disabled]="!isBrutForceAuthenticationEnabled()" required>
-          <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-form-field>
+  <div *ngIf="!inheritMode || !accountSettings.inherited">
+    <h5>Security</h5>
+    <mat-divider></mat-divider>
+    <div class="account-settings-zone ">
+      <div fxLayout="column">
+        <mat-slide-toggle
+          (change)="enableBrutForceAuthenticationDetection($event)"
+          [checked]="isBrutForceAuthenticationEnabled()">
+          Brute Force Detection
+        </mat-slide-toggle>
+        <mat-hint style="font-size: 75%;">Prevent brute force authentication attempts.</mat-hint>
+      </div>
+      <div fxLayout="row">
+        <mat-form-field>
+          <input matInput type="number" placeholder="Max Login Attempts" name="maxLoginAttempts" [(ngModel)]="accountSettings.maxLoginAttempts" [disabled]="!isBrutForceAuthenticationEnabled()" required>
+          <mat-hint>Max login failures after which the account will be blocked.</mat-hint>
+        </mat-form-field>
+      </div>
+      <div fxLayout="row">
+        <mat-form-field>
+          <input matInput type="number" placeholder="Login Attempts Reset Time" name="loginAttemptsResetTime" [(ngModel)]="accountSettings.loginAttemptsResetTime" [disabled]="!isBrutForceAuthenticationEnabled()" required>
+          <mat-hint>Time after which the login attempts will be reset if max attempts has not been reached.</mat-hint>
+        </mat-form-field>
+        <mat-form-field fxFlex style="margin-left: 20px;">
+          <mat-select placeholder="Unit time" name="loginAttemptsResetTimeUnitTime" [(ngModel)]="accountSettings.loginAttemptsResetTimeUnitTime" [disabled]="!isBrutForceAuthenticationEnabled()" required>
+            <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-form-field>
+      </div>
+      <div fxLayout="row">
+        <mat-form-field fxFlex="85">
+          <input matInput type="number" placeholder="Account Blocked Duration" name="accountBlockedDuration" [(ngModel)]="accountSettings.accountBlockedDuration" [disabled]="!isBrutForceAuthenticationEnabled()" required>
+          <mat-hint>Amount of time while the user will be blocked.</mat-hint>
+        </mat-form-field>
+        <mat-form-field fxFlex style="margin-left: 20px;">
+          <mat-select placeholder="Unit time" name="accountBlockedDurationUnitTime" [(ngModel)]="accountSettings.accountBlockedDurationUnitTime" [disabled]="!isBrutForceAuthenticationEnabled()" required>
+            <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-form-field>
+      </div>
     </div>
-    <div fxLayout="row">
-      <mat-form-field fxFlex="85">
-        <input matInput type="number" placeholder="Account Blocked Duration" name="accountBlockedDuration" [(ngModel)]="localAccountSettings.accountBlockedDuration" [disabled]="!isBrutForceAuthenticationEnabled()" required>
-        <mat-hint>Amount of time while the user will be blocked.</mat-hint>
-      </mat-form-field>
-      <mat-form-field fxFlex style="margin-left: 20px;">
-        <mat-select placeholder="Unit time" name="accountBlockedDurationUnitTime" [(ngModel)]="localAccountSettings.accountBlockedDurationUnitTime" [disabled]="!isBrutForceAuthenticationEnabled()" required>
-          <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-form-field>
+
+    <h5>Management</h5>
+    <mat-divider></mat-divider>
+    <div class="account-settings-zone">
+      <div fxLayout="column">
+        <mat-slide-toggle
+          (change)="enableCompleteRegistration($event)"
+          [checked]="isCompleteRegistrationEnabled()">
+          Complete user registration (Reset Password)
+        </mat-slide-toggle>
+        <mat-hint style="font-size: 75%;">If a user renewed his password without completing his registration, finalize the account. The default behavior is to display an error to the user.</mat-hint>
+      </div>
     </div>
   </div>
   <div fxLayout="row" style="margin-top: 50px;">
diff --git a/gravitee-am-ui/src/app/domain/components/account/account-settings.component.scss b/gravitee-am-ui/src/app/domain/components/account/account-settings.component.scss
index 6a89225933..1a9dddf2c5 100644
--- a/gravitee-am-ui/src/app/domain/components/account/account-settings.component.scss
+++ b/gravitee-am-ui/src/app/domain/components/account/account-settings.component.scss
@@ -4,3 +4,9 @@ form {
   border-radius: 2px;
   border: 1px solid rgb(226, 229, 231);
 }
+
+.account-settings-zone {
+  padding: 10px;
+  background-color: #f1f1f1;
+  margin-top: 20px;
+}
diff --git a/gravitee-am-ui/src/app/domain/components/account/account-settings.component.ts b/gravitee-am-ui/src/app/domain/components/account/account-settings.component.ts
index 586d8ad1fc..ff6bcc2b5f 100644
--- a/gravitee-am-ui/src/app/domain/components/account/account-settings.component.ts
+++ b/gravitee-am-ui/src/app/domain/components/account/account-settings.component.ts
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
 import * as moment from "moment";
 
 @Component({
@@ -21,13 +21,12 @@ import * as moment from "moment";
   templateUrl: './account-settings.component.html',
   styleUrls: ['./account-settings.component.scss']
 })
-export class AccountSettingsComponent implements OnInit {
+export class AccountSettingsComponent implements OnInit, OnChanges {
   @Output() onSavedAccountSettings = new EventEmitter<any>();
   @Input() accountSettings: any = {};
   @Input() inheritMode: boolean = false;
   @ViewChild('accountForm') form: any;
   formChanged: boolean = false;
-  localAccountSettings: any;
   private defaultMaxAttempts: number = 10;
   private defaultLoginAttemptsResetTime: number = 12;
   private defaultLoginAttemptsResetTimeUnit: string = "hours";
@@ -37,7 +36,7 @@ export class AccountSettingsComponent implements OnInit {
   constructor() {}
 
   save() {
-    let accountSettings = Object.assign({}, this.localAccountSettings);
+    let accountSettings = Object.assign({}, this.accountSettings);
     if (accountSettings.inherited) {
       accountSettings = { 'inherited' : true };
     } else {
@@ -50,52 +49,68 @@ export class AccountSettingsComponent implements OnInit {
 
     this.onSavedAccountSettings.emit(accountSettings);
     this.formChanged = false;
-    this.form.reset(this.localAccountSettings);
+    this.form.reset(this.accountSettings);
   }
 
   ngOnInit(): void {
-    this.localAccountSettings = Object.assign({}, this.accountSettings || { 'inherited' : this.inheritMode });
+    this.accountSettings = this.accountSettings || { 'inherited' : this.inheritMode };
     this.initDateValues();
   }
 
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.accountSettings.previousValue && changes.accountSettings.currentValue) {
+      this.accountSettings = changes.accountSettings.currentValue;
+      this.initDateValues();
+    }
+  }
+
   enableInheritMode(event) {
-    this.localAccountSettings.inherited = event.checked;
+    this.accountSettings.inherited = event.checked;
     this.formChanged = true;
   }
 
   isInherited() {
-    return this.localAccountSettings && this.localAccountSettings.inherited;
+    return this.accountSettings && this.accountSettings.inherited;
   }
 
   enableBrutForceAuthenticationDetection(event) {
-    this.localAccountSettings.loginAttemptsDetectionEnabled = event.checked;
+    this.accountSettings.loginAttemptsDetectionEnabled = event.checked;
     this.formChanged = true;
 
     // apply default values
-    this.localAccountSettings.maxLoginAttempts = this.localAccountSettings.maxLoginAttempts || this.defaultMaxAttempts;
-    this.localAccountSettings.loginAttemptsResetTime = this.localAccountSettings.loginAttemptsResetTime || this.defaultLoginAttemptsResetTime;
-    this.localAccountSettings.loginAttemptsResetTimeUnitTime = this.localAccountSettings.loginAttemptsResetTimeUnitTime || this.defaultLoginAttemptsResetTimeUnit;
-    this.localAccountSettings.accountBlockedDuration = this.localAccountSettings.accountBlockedDuration || this.defaultAccountBlockedDuration;
-    this.localAccountSettings.accountBlockedDurationUnitTime = this.localAccountSettings.accountBlockedDurationUnitTime || this.defaultAccountBlockedDurationUnit;
+    this.accountSettings.maxLoginAttempts = this.accountSettings.maxLoginAttempts || this.defaultMaxAttempts;
+    this.accountSettings.loginAttemptsResetTime = this.accountSettings.loginAttemptsResetTime || this.defaultLoginAttemptsResetTime;
+    this.accountSettings.loginAttemptsResetTimeUnitTime = this.accountSettings.loginAttemptsResetTimeUnitTime || this.defaultLoginAttemptsResetTimeUnit;
+    this.accountSettings.accountBlockedDuration = this.accountSettings.accountBlockedDuration || this.defaultAccountBlockedDuration;
+    this.accountSettings.accountBlockedDurationUnitTime = this.accountSettings.accountBlockedDurationUnitTime || this.defaultAccountBlockedDurationUnit;
   }
 
   isBrutForceAuthenticationEnabled() {
-    return this.localAccountSettings && this.localAccountSettings.loginAttemptsDetectionEnabled;
+    return this.accountSettings && this.accountSettings.loginAttemptsDetectionEnabled;
+  }
+
+  enableCompleteRegistration(event) {
+    this.accountSettings.completeRegistrationWhenResetPassword = event.checked;
+    this.formChanged = true;
+  }
+
+  isCompleteRegistrationEnabled() {
+    return this.accountSettings && this.accountSettings.completeRegistrationWhenResetPassword;
   }
 
   formIsValid() {
-    if (this.localAccountSettings.loginAttemptsDetectionEnabled) {
-      if (this.localAccountSettings.maxLoginAttempts < 1) {
+    if (this.accountSettings.loginAttemptsDetectionEnabled) {
+      if (this.accountSettings.maxLoginAttempts < 1) {
         return false;
       }
-      if (this.localAccountSettings.loginAttemptsResetTime < 1) {
+      if (this.accountSettings.loginAttemptsResetTime < 1) {
         return false;
-      } else if (!this.localAccountSettings.loginAttemptsResetTimeUnitTime) {
+      } else if (!this.accountSettings.loginAttemptsResetTimeUnitTime) {
         return false;
       }
-      if (this.localAccountSettings.accountBlockedDuration < 1) {
+      if (this.accountSettings.accountBlockedDuration < 1) {
         return false;
-      } else if (!this.localAccountSettings.accountBlockedDurationUnitTime) {
+      } else if (!this.accountSettings.accountBlockedDurationUnitTime) {
         return false;
       }
     }
@@ -103,16 +118,16 @@ export class AccountSettingsComponent implements OnInit {
   }
 
   private initDateValues() {
-    if (this.localAccountSettings.loginAttemptsResetTime > 0) {
-      let loginAttemptsResetTime = this.getHumanizeDuration(this.localAccountSettings.loginAttemptsResetTime);
-      this.localAccountSettings.loginAttemptsResetTime = loginAttemptsResetTime[0];
-      this.localAccountSettings.loginAttemptsResetTimeUnitTime = loginAttemptsResetTime[1];
+    if (this.accountSettings.loginAttemptsResetTime > 0) {
+      let loginAttemptsResetTime = this.getHumanizeDuration(this.accountSettings.loginAttemptsResetTime);
+      this.accountSettings.loginAttemptsResetTime = loginAttemptsResetTime[0];
+      this.accountSettings.loginAttemptsResetTimeUnitTime = loginAttemptsResetTime[1];
     }
 
-    if (this.localAccountSettings.accountBlockedDuration > 0) {
-      let accountBlockedDuration = this.getHumanizeDuration(this.localAccountSettings.accountBlockedDuration);
-      this.localAccountSettings.accountBlockedDuration = accountBlockedDuration[0];
-      this.localAccountSettings.accountBlockedDurationUnitTime = accountBlockedDuration[1];
+    if (this.accountSettings.accountBlockedDuration > 0) {
+      let accountBlockedDuration = this.getHumanizeDuration(this.accountSettings.accountBlockedDuration);
+      this.accountSettings.accountBlockedDuration = accountBlockedDuration[0];
+      this.accountSettings.accountBlockedDurationUnitTime = accountBlockedDuration[1];
     }
   }
 
diff --git a/gravitee-am-ui/src/app/domain/settings/account/account.component.html b/gravitee-am-ui/src/app/domain/settings/account/account.component.html
index c93f56f210..4403be7817 100644
--- a/gravitee-am-ui/src/app/domain/settings/account/account.component.html
+++ b/gravitee-am-ui/src/app/domain/settings/account/account.component.html
@@ -19,7 +19,7 @@
   <h1>User Accounts</h1>
   <div class="domain-account-content">
     <div fxFlex="70">
-      <app-account-settings [accountSettings]="domain.accountSettings" [inheritMode]="false" (onSavedAccountSettings)="updateAccountSettings($event)"></app-account-settings>
+      <app-account-settings [accountSettings]="accountSettings" [inheritMode]="false" (onSavedAccountSettings)="updateAccountSettings($event)"></app-account-settings>
     </div>
     <div class="domain-account-description" fxFlex>
       <h3>User Accounts settings</h3>
diff --git a/gravitee-am-ui/src/app/domain/settings/account/account.component.ts b/gravitee-am-ui/src/app/domain/settings/account/account.component.ts
index 9416982a0b..84c78e582c 100644
--- a/gravitee-am-ui/src/app/domain/settings/account/account.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/account/account.component.ts
@@ -27,6 +27,7 @@ import {ActivatedRoute, Router} from "@angular/router";
 export class DomainSettingsAccountComponent implements OnInit {
   domainId: string;
   domain: any = {};
+  accountSettings: any;
 
   constructor(private domainService: DomainService,
               private snackbarService: SnackbarService,
@@ -39,11 +40,14 @@ export class DomainSettingsAccountComponent implements OnInit {
       this.domainId = AppConfig.settings.authentication.domainId;
     }
     this.domain = this.route.snapshot.data['domain'];
+    this.accountSettings = Object.assign({}, this.domain.accountSettings);
   }
   updateAccountSettings(accountSettings) {
-    this.domain.accountSettings = accountSettings;
+    // force inherit false
+    accountSettings.inherited = false;
     this.domainService.patchAccountSettings(this.domainId, accountSettings).subscribe(data => {
       this.domain = data;
+      this.route.snapshot.data['domain'] = this.domain;
       this.snackbarService.open("User Accounts Settings updated");
     });
   }
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 7517a8009e..65d1b684d8 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
@@ -63,6 +63,14 @@
           </mat-form-field>
         </div>
       </div>
+
+      <mat-slide-toggle
+        (change)="enableScopeDiscovery($event)"
+        [checked]="isDiscovery()">
+        Display scope into openid discovery endpoint
+      </mat-slide-toggle>
+      <mat-hint style="font-size: 75%;">Enable scope to be displayed into the openid discovery endpoint under <i>:domain/oidc/.well-known/openid-configuration</i></mat-hint>
+
       <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) || formIsInvalid()" type="submit">CREATE</button>
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 a4dc5f12a7..74f29891e3 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
@@ -21,6 +21,7 @@
         padding: 10px;
         background-color: #f1f1f1;
         margin-top: 20px;
+        margin-bottom: 20px;
         h4 {
           margin: 5px 0;
         }
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 944fa2c0a6..45ca10a1bf 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
@@ -65,4 +65,12 @@ export class ScopeCreationComponent implements OnInit {
   getScopeExpiry() {
     return (this.scope.expiresIn) ? moment.duration(this.scope.expiresIn, 'seconds').humanize() : 'no time set';
   }
+
+  enableScopeDiscovery(event) {
+    this.scope.discovery = event.checked;
+  }
+
+  isDiscovery() {
+    return this.scope.discovery;
+  }
 }
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 e822f32289..b6f1606d6a 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
@@ -65,6 +65,13 @@
           </div>
         </div>
 
+        <mat-slide-toggle
+          (change)="enableScopeDiscovery($event)"
+          [checked]="isDiscovery()">
+          Display scope into openid discovery endpoint
+        </mat-slide-toggle>
+        <mat-hint style="font-size: 75%;">Enable scope to be displayed into the openid discovery endpoint under <i>:domain/oidc/.well-known/openid-configuration</i></mat-hint>
+
         <div fxLayout="row" class="scope-form-actions">
           <button mat-raised-button [disabled]="(!scopeForm.valid || scopeForm.pristine || formIsInvalid()) && !formChanged" type="submit">SAVE</button>
         </div>
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 7dc54239fa..a21385ffe5 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
@@ -21,6 +21,7 @@
         padding: 10px;
         background-color: #f1f1f1;
         margin-top: 20px;
+        margin-bottom: 20px;
         h4 {
           margin: 5px 0;
         }
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 2ed46dd591..e12b2b5d69 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
@@ -83,6 +83,15 @@ export class ScopeComponent implements OnInit {
     this.formChanged = true;
   }
 
+  enableScopeDiscovery(event) {
+    this.scope.discovery = event.checked;
+    this.formChanged = true;
+  }
+
+  isDiscovery() {
+    return this.scope.discovery;
+  }
+
   delete(event) {
     event.preventDefault();
     this.dialogService
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 338c478ed1..bfc1989a04 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,17 @@
           {{row.name}}
         </ng-template>
       </ngx-datatable-column>
+      <ngx-datatable-column name="Discovery" [flexGrow]="1">
+        <ng-template let-row="row" ngx-datatable-cell-template>
+          <span>
+            <mat-slide-toggle
+              (change)="enableScopeDiscovery(row.id, $event)"
+              [checked]="row.discovery"
+              [disabled]="row.system">
+            </mat-slide-toggle>
+          </span>
+        </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>
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 ca727370fc..4cc701738e 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
@@ -43,7 +43,7 @@ export class DomainSettingsScopesComponent implements OnInit {
   }
 
   get isEmpty() {
-    return !this.scopes || this.scopes.length == 0;
+    return !this.scopes || this.scopes.length === 0;
   }
 
   delete(id, event) {
@@ -64,4 +64,10 @@ export class DomainSettingsScopesComponent implements OnInit {
     return expiresIn ? moment.duration(expiresIn, 'seconds').humanize() : 'no time set';
   }
 
+  enableScopeDiscovery(id, event) {
+    this.scopeService.patchDiscovery(this.domainId, id, event.checked).subscribe(response => {
+      this.snackbarService.open('Scope updated');
+      this.loadScopes();
+    });
+  }
 }
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts
index 75995ef352..8b3bc8c9c7 100644
--- a/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts
@@ -154,8 +154,18 @@ export class UserProfileComponent implements OnInit {
   }
 
   enableUser(event) {
-    this.user.enabled = event.checked;
-    this.formChanged = true;
+    this.dialogService
+      .confirm( (event.checked ? 'Enable' : 'Disable') + ' User', 'Are you sure you wand to ' + (event.checked ? 'enable' : 'disable') + ' the user ?')
+      .subscribe(res => {
+        if (res) {
+          this.userService.updateStatus(this.domainId, this.user.id, event.checked).subscribe(() => {
+            this.user.enabled = event.checked;
+            this.snackbarService.open('User ' + (event.checked ? 'enabled' : 'disabled'));
+          });
+        } else {
+          event.source.checked = true;
+        }
+      });
   }
 
   isUserEnabled() {
diff --git a/gravitee-am-ui/src/app/services/scope.service.ts b/gravitee-am-ui/src/app/services/scope.service.ts
index 3a6d72accf..6e948f7a2b 100644
--- a/gravitee-am-ui/src/app/services/scope.service.ts
+++ b/gravitee-am-ui/src/app/services/scope.service.ts
@@ -40,7 +40,14 @@ export class ScopeService {
     return this.http.put<any>(this.scopes + domainId + "/scopes/" + id, {
       'name' : scope.name,
       'description' : scope.description,
-      'expiresIn' : scope.expiresIn
+      'expiresIn' : scope.expiresIn,
+      'discovery' : scope.discovery
+    });
+  }
+
+  patchDiscovery(domainId, id, discovery): Observable<any> {
+    return this.http.patch<any>(this.scopes + domainId + "/scopes/" + id, {
+      'discovery' : discovery
     });
   }
 
diff --git a/gravitee-am-ui/src/app/services/user.service.ts b/gravitee-am-ui/src/app/services/user.service.ts
index 3c53e93893..76b8b3116c 100644
--- a/gravitee-am-ui/src/app/services/user.service.ts
+++ b/gravitee-am-ui/src/app/services/user.service.ts
@@ -47,6 +47,12 @@ export class UserService {
     });
   }
 
+  updateStatus(domainId, id, status): Observable<any> {
+    return this.http.put<any>(this.usersURL + domainId + "/users/" + id + '/status', {
+      'enabled' : status
+    });
+  }
+
   delete(domainId, id): Observable<any> {
     return this.http.delete<any>(this.usersURL + domainId + "/users/" + id);
   }
diff --git a/postman/collections/graviteeio-am-openid-discovery-collection.json b/postman/collections/graviteeio-am-openid-discovery-collection.json
index ceb5a14a4d..0389a0e016 100644
--- a/postman/collections/graviteeio-am-openid-discovery-collection.json
+++ b/postman/collections/graviteeio-am-openid-discovery-collection.json
@@ -286,6 +286,7 @@
 									"    pm.expect(body).to.have.property(\"require_request_uri_registration\");",
 									"    ",
 									"    pm.expect(body.code_challenge_methods_supported).to.eql([ 'plain','S256' ]);",
+									"    pm.expect(body.scopes_supported).to.eql([ 'address','email','offline_access','openid','phone','profile']);",
 									"});"
 								],
 								"type": "text/javascript"
diff --git a/postman/collections/graviteeio-am-scope-management-collection.json b/postman/collections/graviteeio-am-scope-management-collection.json
new file mode 100644
index 0000000000..b9a68893d4
--- /dev/null
+++ b/postman/collections/graviteeio-am-scope-management-collection.json
@@ -0,0 +1,1223 @@
+{
+	"info": {
+		"_postman_id": "84ad4c35-c898-4975-b32e-9fcda726b1d6",
+		"name": "Gravitee.io - AM - Scope Management",
+		"description": "Test openid connect discovery specifications: https://openid.net/specs/openid-connect-discovery-1_0.html",
+		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+	},
+	"item": [
+		{
+			"name": "Prepare",
+			"item": [
+				{
+					"name": "Generate admin token",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "571e9b59-b9e7-452c-9469-9786ded290a6",
+								"exec": [
+									"",
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"var token = JSON.parse(responseBody);",
+									"pm.environment.set('token', token.access_token);"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Authorization",
+								"value": "Basic YWRtaW46YWRtaW5hZG1pbg=="
+							}
+						],
+						"body": {
+							"mode": "urlencoded",
+							"urlencoded": [
+								{
+									"key": "grant_type",
+									"value": "password",
+									"type": "text"
+								},
+								{
+									"key": "username",
+									"value": "admin",
+									"type": "text"
+								},
+								{
+									"key": "password",
+									"value": "adminadmin",
+									"type": "text"
+								}
+							]
+						},
+						"url": {
+							"raw": "{{management_url}}/admin/token",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"admin",
+								"token"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create scope domain",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "b286f0fa-cfcc-45b9-863a-1dbc87fdf835",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});",
+									"",
+									"var jsonData = pm.response.json();",
+									"pm.environment.set('domain', jsonData.id);"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\"name\": \"scope\", \n\t\"description\": \"test scope management\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								""
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Start domain",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "bfe78ac1-144a-4bbd-abf9-55e160e723bf",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"enabled\": true\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}"
+							]
+						}
+					},
+					"response": []
+				}
+			]
+		},
+		{
+			"name": "Invalid Case",
+			"item": [
+				{
+					"name": "Create - Missing property",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 400\", function () {",
+									"    pm.response.to.have.status(400);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.message).to.eql('[description: may not be null]');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"key\": \"admin\",\n  \"name\": \"admin\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create already existing scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 400\", function () {",
+									"    pm.response.to.have.status(400);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.message).to.contains('A scope [email] already exists for domain');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"key\": \"email\",\n  \"name\": \"should not be created\",\n  \"description\": \"should not be created\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update non existing scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 404\", function () {",
+									"    pm.response.to.have.status(404);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"    pm.expect(body.message).to.contains('can not be found');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PUT",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"name\": \"shouldNotBeUpdated\",\n  \"description\": \"shouldNotBeUpdated\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes/nonExistingScope",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes",
+								"nonExistingScope"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update - Missing name property",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 400\", function () {",
+									"    pm.response.to.have.status(400);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"    pm.expect(body.message).to.eql('[name: may not be null]');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PUT",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"discovery\": true,\n  \"description\": \"wallet information updated\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes/nonExistingScope",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes",
+								"nonExistingScope"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete non existing scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 404\", function () {",
+									"    pm.response.to.have.status(404);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"    pm.expect(body.message).to.contains('can not be found');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"name\": \"shouldNotBeUpdated\",\n  \"description\": \"shouldNotBeUpdated\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes/nonExistingScope",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes",
+								"nonExistingScope"
+							]
+						}
+					},
+					"response": []
+				}
+			]
+		},
+		{
+			"name": "Nominal Case",
+			"item": [
+				{
+					"name": "Get scopes",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body[0].key).to.eql('address');",
+									"    pm.expect(body[0].system).to.eql(true);",
+									"    ",
+									"    pm.environment.set('addressScopeId', body[0].id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.key).to.eql('admin');",
+									"    pm.expect(body.discovery).to.eql(false);",
+									"    ",
+									"    pm.environment.set('adminScopeId', body.id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"key\": \"admin\",\n  \"name\": \"admin\",\n  \"description\": \"admin non public scope\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.discovery).to.eql(false);",
+									"    pm.expect(body.name).to.eql('admin');",
+									"    pm.expect(body.description).to.eql('admin non public scope');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes/{{adminScopeId}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes",
+								"{{adminScopeId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Patch scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.discovery).to.eql(true);",
+									"    pm.expect(body.name).to.eql('admin name patched');",
+									"    pm.expect(body.description).to.eql('admin information patched');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"discovery\": true,\n  \"name\": \"admin name patched\",\n  \"description\": \"admin information patched\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes/{{adminScopeId}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes",
+								"{{adminScopeId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.discovery).to.eql(true);//should not be overrided when missing in the body",
+									"    pm.expect(body.name).to.eql('admin name updated');",
+									"    pm.expect(body.description).to.eql('admin information updated');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PUT",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"name\": \"admin name updated\",\n  \"description\": \"admin information updated\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes/{{adminScopeId}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes",
+								"{{adminScopeId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 204\", function () {",
+									"    pm.response.to.have.status(204);",
+									"});",
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"discovery\": true,\n  \"description\": \"wallet information updated\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes/{{adminScopeId}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes",
+								"{{adminScopeId}}"
+							]
+						}
+					},
+					"response": []
+				}
+			]
+		},
+		{
+			"name": "System scope",
+			"item": [
+				{
+					"name": "System scope discovery",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.discovery).to.eql(true);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"discovery\": false,\n  \"description\": \"Discovery should stay to true\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes/{{addressScopeId}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes",
+								"{{addressScopeId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "System scope discovery",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.discovery).to.eql(true);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PUT",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"discovery\": false,\n  \"name\": \"Address\",\n  \"description\": \"Discovery should stay to true\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes/{{addressScopeId}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes",
+								"{{addressScopeId}}"
+							]
+						}
+					},
+					"response": []
+				}
+			]
+		},
+		{
+			"name": "Scope Discovery",
+			"item": [
+				{
+					"name": "Create admin scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});",
+									"",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.key).to.eql('admin');",
+									"    pm.expect(body.discovery).to.eql(false);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"key\": \"admin\",\n  \"name\": \"admin\",\n  \"description\": \"admin non public scope\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create body scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.key).to.eql('body');",
+									"    pm.expect(body.discovery).to.eql(true);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"key\": \"body\",\n  \"name\": \"body\",\n  \"description\": \"body information\",\n  \"discovery\": true\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create wallet scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.key).to.eql('wallet');",
+									"    pm.expect(body.discovery).to.eql(false);",
+									"    ",
+									"    pm.environment.set('walletScopeId', body.id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"key\": \"wallet\",\n  \"name\": \"wallet\",\n  \"description\": \"wallet information\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Patch wallet scope",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a9d1a08f-42e1-48a8-a551-aeda99bc04e1",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Check scope properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"",
+									"    pm.expect(body.discovery).to.eql(true);",
+									"    pm.expect(body.name).to.eql('wallet');",
+									"    pm.expect(body.description).to.eql('wallet information updated');",
+									"});",
+									"",
+									"// wait for sync process",
+									"setTimeout(function(){}, 5000);"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"discovery\": true,\n  \"description\": \"wallet information updated\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/scopes/{{walletScopeId}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"scopes",
+								"{{walletScopeId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "well-known/openid-configuration",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "5332b6f5-7419-4792-a30f-cce968d3d67e",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Check scope discovery properties\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"    pm.expect(body.scopes_supported).to.eql([ 'address','body','email','offline_access','openid','phone','profile','wallet']);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{gateway_url}}/{{domain}}/oidc/.well-known/openid-configuration",
+							"host": [
+								"{{gateway_url}}"
+							],
+							"path": [
+								"{{domain}}",
+								"oidc",
+								".well-known",
+								"openid-configuration"
+							]
+						}
+					},
+					"response": []
+				}
+			]
+		},
+		{
+			"name": "Delete domain",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"id": "abfa8366-3ee2-45b0-b658-0040b79d565c",
+						"exec": [
+							"pm.test(\"Status code is 204\", function () {",
+							"    pm.response.to.have.status(204);",
+							"});"
+						],
+						"type": "text/javascript"
+					}
+				}
+			],
+			"request": {
+				"method": "DELETE",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json"
+					},
+					{
+						"key": "Authorization",
+						"value": "Bearer {{token}}"
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": ""
+				},
+				"url": {
+					"raw": "{{management_url}}/management/domains/{{domain}}",
+					"host": [
+						"{{management_url}}"
+					],
+					"path": [
+						"management",
+						"domains",
+						"{{domain}}"
+					]
+				}
+			},
+			"response": []
+		}
+	]
+}
\ No newline at end of file


Test patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/test/java/io/gravitee/am/gateway/handler/root/service/user/UserServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/test/java/io/gravitee/am/gateway/handler/root/service/user/UserServiceTest.java
new file mode 100644
index 0000000000..fb5c38f709
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/test/java/io/gravitee/am/gateway/handler/root/service/user/UserServiceTest.java
@@ -0,0 +1,194 @@
+/**
+ * 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.root.service.user;
+
+import io.gravitee.am.gateway.handler.common.auth.idp.IdentityProviderManager;
+import io.gravitee.am.gateway.handler.root.service.user.impl.UserServiceImpl;
+import io.gravitee.am.identityprovider.api.UserProvider;
+import io.gravitee.am.model.Client;
+import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.User;
+import io.gravitee.am.model.account.AccountSettings;
+import io.gravitee.am.repository.management.api.UserRepository;
+import io.gravitee.am.service.AuditService;
+import io.gravitee.am.service.LoginAttemptService;
+import io.gravitee.am.service.exception.authentication.AccountInactiveException;
+import io.reactivex.Completable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.reactivex.observers.TestObserver;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.Collections;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class UserServiceTest {
+
+    @InjectMocks
+    private UserService userService = new UserServiceImpl();
+
+    @Mock
+    private Domain domain;
+
+    @Mock
+    private IdentityProviderManager identityProviderManager;
+
+    @Mock
+    private UserRepository userRepository;
+
+    @Mock
+    private LoginAttemptService loginAttemptService;
+
+    @Mock
+    private AuditService auditService;
+
+    @Test
+    public void shouldNotResetPassword_userInactive() {
+        Client client = mock(Client.class);
+        User user = mock(User.class);
+
+        when(user.isInactive()).thenReturn(true);
+
+        TestObserver testObserver = userService.resetPassword(user, client).test();
+        testObserver.assertNotComplete();
+        testObserver.assertError(AccountInactiveException.class);
+    }
+
+    @Test
+    public void shouldResetPassword_userInactive_forceRegistration() {
+        Client client = mock(Client.class);
+
+        User user = mock(User.class);
+        when(user.getExternalId()).thenReturn("external-id");
+        when(user.getUsername()).thenReturn("username");
+        when(user.isInactive()).thenReturn(true);
+        when(user.getSource()).thenReturn("default-idp");
+
+        io.gravitee.am.identityprovider.api.User idpUser = mock(io.gravitee.am.identityprovider.api.User.class);
+
+        AccountSettings accountSettings = mock(AccountSettings.class);
+        when(accountSettings.isCompleteRegistrationWhenResetPassword()).thenReturn(true);
+
+        UserProvider userProvider = mock(UserProvider.class);
+        when(userProvider.update(anyString(), any())).thenReturn(Single.just(idpUser));
+
+
+        when(domain.getAccountSettings()).thenReturn(accountSettings);
+        when(identityProviderManager.getUserProvider(user.getSource())).thenReturn(Maybe.just(userProvider));
+        when(userRepository.update(any())).thenReturn(Single.just(user));
+        when(loginAttemptService.reset(any())).thenReturn(Completable.complete());
+
+        TestObserver testObserver = userService.resetPassword(user, client).test();
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+    }
+
+    @Test
+    public void shouldResetPassword_userActive() {
+        Client client = mock(Client.class);
+
+        User user = mock(User.class);
+        when(user.getExternalId()).thenReturn("external-id");
+        when(user.getUsername()).thenReturn("username");
+        when(user.isInactive()).thenReturn(false);
+        when(user.getSource()).thenReturn("default-idp");
+
+        io.gravitee.am.identityprovider.api.User idpUser = mock(io.gravitee.am.identityprovider.api.User.class);
+
+        UserProvider userProvider = mock(UserProvider.class);
+        when(userProvider.update(anyString(), any())).thenReturn(Single.just(idpUser));
+
+        when(identityProviderManager.getUserProvider(user.getSource())).thenReturn(Maybe.just(userProvider));
+        when(userRepository.update(any())).thenReturn(Single.just(user));
+        when(loginAttemptService.reset(any())).thenReturn(Completable.complete());
+
+        TestObserver testObserver = userService.resetPassword(user, client).test();
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+    }
+
+    @Test
+    public void shouldNotForgotPassword_userInactive() {
+        when(domain.getId()).thenReturn("domain-id");
+
+        Client client = mock(Client.class);
+
+        User user = mock(User.class);
+        when(user.getEmail()).thenReturn("test@test.com");
+        when(user.isInternal()).thenReturn(true);
+        when(user.isInactive()).thenReturn(true);
+
+        when(userRepository.findByDomainAndEmail(domain.getId(), user.getEmail())).thenReturn(Single.just(Collections.singletonList(user)));
+
+        TestObserver testObserver = userService.forgotPassword(user.getEmail(), client).test();
+        testObserver.assertNotComplete();
+        testObserver.assertError(AccountInactiveException.class);
+    }
+
+    @Test
+    public void shouldForgotPassword_userInactive_forceRegistration() {
+        Client client = mock(Client.class);
+
+        User user = mock(User.class);
+        when(user.getEmail()).thenReturn("test@test.com");
+        when(user.isInternal()).thenReturn(true);
+        when(user.isInactive()).thenReturn(true);
+
+        AccountSettings accountSettings = mock(AccountSettings.class);
+        when(accountSettings.isCompleteRegistrationWhenResetPassword()).thenReturn(true);
+
+        when(domain.getId()).thenReturn("domain-id");
+        when(domain.getAccountSettings()).thenReturn(accountSettings);
+
+        when(userRepository.findByDomainAndEmail(domain.getId(), user.getEmail())).thenReturn(Single.just(Collections.singletonList(user)));
+
+        TestObserver testObserver = userService.forgotPassword(user.getEmail(), client).test();
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+    }
+
+    @Test
+    public void shouldForgotPassword_userActive() {
+        Client client = mock(Client.class);
+
+        User user = mock(User.class);
+        when(user.getEmail()).thenReturn("test@test.com");
+        when(user.isInternal()).thenReturn(true);
+        when(user.isInactive()).thenReturn(false);
+
+        when(domain.getId()).thenReturn("domain-id");
+
+        when(userRepository.findByDomainAndEmail(domain.getId(), user.getEmail())).thenReturn(Single.just(Collections.singletonList(user)));
+
+        TestObserver testObserver = userService.forgotPassword(user.getEmail(), client).test();
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+    }
+
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/scope/ScopeServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/scope/ScopeServiceTest.java
new file mode 100644
index 0000000000..a97f14b4ad
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/scope/ScopeServiceTest.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.oauth2.service.scope;
+
+import io.gravitee.am.gateway.handler.oauth2.service.scope.impl.ScopeServiceImpl;
+import io.gravitee.am.model.oauth2.Scope;
+import io.reactivex.observers.TestObserver;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ScopeServiceTest {
+
+    @InjectMocks
+    private ScopeService scopeService = new ScopeServiceImpl();
+
+    @Mock
+    private ScopeManager scopeManager;
+
+    @Before
+    public void setUp() {
+        Scope one = new Scope();
+        one.setKey("one");
+
+        Scope two = new Scope();
+        two.setKey("two");
+        two.setDiscovery(true);
+
+        when(scopeManager.findAll()).thenReturn(new HashSet<>(Arrays.asList(one,two)));
+    }
+
+    @Test
+    public void getAll() {
+        TestObserver testObserver = scopeService.getAll().test();
+
+        verify(scopeManager, times(1)).findAll();
+        testObserver.assertNoErrors();
+        testObserver.assertComplete();
+        testObserver.assertValue(list -> ((Set)list).size()==2);
+    }
+
+    @Test
+    public void getDiscoveryScope() {
+        assertEquals(1,scopeService.getDiscoveryScope().size());
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/OpenIDScopeUpgraderTest.java b/gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/OpenIDScopeUpgraderTest.java
new file mode 100644
index 0000000000..3542721328
--- /dev/null
+++ b/gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/OpenIDScopeUpgraderTest.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.management.service;
+
+import io.gravitee.am.management.service.impl.upgrades.OpenIDScopeUpgrader;
+import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.oauth2.Scope;
+import io.gravitee.am.service.DomainService;
+import io.gravitee.am.service.ScopeService;
+import io.gravitee.am.service.model.NewSystemScope;
+import io.gravitee.am.service.model.UpdateSystemScope;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class OpenIDScopeUpgraderTest {
+
+    @InjectMocks
+    private OpenIDScopeUpgrader openIDScopeUpgrader = new OpenIDScopeUpgrader();
+
+    @Mock
+    private DomainService domainService;
+
+    @Mock
+    private ScopeService scopeService;
+
+    @Mock
+    private Domain domain;
+
+    private static final String DOMAIN_ID = "domainId";
+
+    @Before
+    public void setUp() {
+        when(domain.getId()).thenReturn(DOMAIN_ID);
+        when(domainService.findAll()).thenReturn(Single.just(new HashSet<>(Arrays.asList(domain))));
+    }
+
+    @Test
+    public void shouldCreateSystemScope() {
+        when(scopeService.findByDomainAndKey(eq(DOMAIN_ID), anyString())).thenReturn(Maybe.empty());
+        when(scopeService.create(anyString(),any(NewSystemScope.class))).thenReturn(Single.just(new Scope()));
+
+        assertTrue(openIDScopeUpgrader.upgrade());
+        verify(scopeService, times(io.gravitee.am.common.oidc.Scope.values().length)).create(anyString(), any(NewSystemScope.class));
+    }
+
+    @Test
+    public void shouldUpdateSystemScope() {
+        Scope openId = new Scope();
+        openId.setId("1");
+        openId.setSystem(false);//expect to be updated because not set as system
+        openId.setKey("openid");
+
+        Scope phone = new Scope();
+        phone.setId("2");
+        phone.setSystem(true);
+        phone.setKey("phone");
+        phone.setDiscovery(false);//expect to be updated because not same discovery value
+
+        Scope email = new Scope();
+        email.setId("3");
+        email.setSystem(true);//expect not to be updated
+        email.setKey("email");
+        email.setDiscovery(true);
+
+        when(scopeService.findByDomainAndKey(eq(DOMAIN_ID), anyString())).thenReturn(Maybe.empty());
+        when(scopeService.findByDomainAndKey(DOMAIN_ID, "openid")).thenReturn(Maybe.just(openId));
+        when(scopeService.findByDomainAndKey(DOMAIN_ID, "phone")).thenReturn(Maybe.just(phone));
+        when(scopeService.findByDomainAndKey(DOMAIN_ID, "email")).thenReturn(Maybe.just(email));
+        when(scopeService.create(anyString(),any(NewSystemScope.class))).thenReturn(Single.just(new Scope()));
+        when(scopeService.update(anyString(), anyString(), any(UpdateSystemScope.class))).thenReturn(Single.just(new Scope()));
+
+        assertTrue(openIDScopeUpgrader.upgrade());
+        verify(scopeService, times(io.gravitee.am.common.oidc.Scope.values().length-3)).create(anyString(), any(NewSystemScope.class));
+        verify(scopeService, times(2)).update(anyString(), anyString(), any(UpdateSystemScope.class));
+    }
+}
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 331f5d0a34..f398309aef 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
@@ -26,24 +26,30 @@ import io.gravitee.am.service.exception.*;
 import io.gravitee.am.service.impl.ScopeServiceImpl;
 import io.gravitee.am.service.model.NewScope;
 import io.gravitee.am.service.model.PatchClient;
+import io.gravitee.am.service.model.PatchScope;
 import io.gravitee.am.service.model.UpdateRole;
+import io.gravitee.am.service.model.UpdateScope;
+import io.gravitee.am.service.model.UpdateSystemScope;
 import io.reactivex.Completable;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
 import io.reactivex.observers.TestObserver;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatcher;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Mockito;
-import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedList;
+import java.util.Optional;
 import java.util.Set;
 
+import static org.junit.Assert.*;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.*;
 
@@ -228,6 +234,236 @@ public class ScopeServiceTest {
         verify(scopeRepository, never()).create(any(Scope.class));
     }
 
+    @Test
+    public void shouldPatch_systemScope_discoveryNotReplaced() {
+        PatchScope patch = new PatchScope();
+        patch.setDiscovery(Optional.of(true));
+        patch.setName(Optional.of("name"));
+
+        final String scopeId = "toPatchId";
+
+        Scope toPatch = new Scope();
+        toPatch.setId(scopeId);
+        toPatch.setSystem(true);
+        toPatch.setDiscovery(false);
+        toPatch.setName("oldName");
+        toPatch.setDescription("oldDescription");
+
+        ArgumentCaptor<Scope> argument = ArgumentCaptor.forClass(Scope.class);
+
+        when(scopeRepository.findById(scopeId)).thenReturn(Maybe.just(toPatch));
+        when(scopeRepository.update(argument.capture())).thenReturn(Single.just(new Scope()));
+        when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
+
+        TestObserver testObserver = scopeService.patch(DOMAIN,scopeId, patch).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        verify(scopeRepository, times(1)).update(any(Scope.class));
+        assertNotNull(argument.getValue());
+        assertEquals("name",argument.getValue().getName());
+        assertEquals("oldDescription",argument.getValue().getDescription());
+        assertFalse(argument.getValue().isDiscovery());
+    }
+
+    @Test
+    public void shouldPatch_nonSystemScope_discoveryNotReplaced() {
+        PatchScope patch = new PatchScope();
+        patch.setDiscovery(Optional.of(true));
+        patch.setName(Optional.of("name"));
+
+        final String scopeId = "toPatchId";
+
+        Scope toPatch = new Scope();
+        toPatch.setId(scopeId);
+        toPatch.setSystem(false);
+        toPatch.setDiscovery(false);
+        toPatch.setName("oldName");
+        toPatch.setDescription("oldDescription");
+
+        ArgumentCaptor<Scope> argument = ArgumentCaptor.forClass(Scope.class);
+
+        when(scopeRepository.findById(scopeId)).thenReturn(Maybe.just(toPatch));
+        when(scopeRepository.update(argument.capture())).thenReturn(Single.just(new Scope()));
+        when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
+
+        TestObserver testObserver = scopeService.patch(DOMAIN,scopeId, patch).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        verify(scopeRepository, times(1)).update(any(Scope.class));
+        assertNotNull(argument.getValue());
+        assertEquals("name",argument.getValue().getName());
+        assertEquals("oldDescription",argument.getValue().getDescription());
+        assertTrue(argument.getValue().isDiscovery());
+    }
+
+    @Test
+    public void shouldNotPatch() {
+        Scope toPatch = new Scope();
+        toPatch.setId("toPatchId");
+
+        when(scopeRepository.findById("toPatchId")).thenReturn(Maybe.error(TechnicalException::new));
+
+        TestObserver testObserver = scopeService.patch(DOMAIN,"toPatchId", new PatchScope()).test();
+
+        testObserver.assertError(TechnicalManagementException.class);
+        testObserver.assertNotComplete();
+    }
+
+    @Test
+    public void shouldUpdate_systemScope_discoveryNotReplaced() {
+        UpdateScope updateScope = new UpdateScope();
+        updateScope.setDiscovery(true);
+        updateScope.setName("name");
+
+        final String scopeId = "toUpdateId";
+
+        Scope toUpdate = new Scope();
+        toUpdate.setId(scopeId);
+        toUpdate.setSystem(true);
+        toUpdate.setDiscovery(false);
+        toUpdate.setName("oldName");
+        toUpdate.setDescription("oldDescription");
+
+        ArgumentCaptor<Scope> argument = ArgumentCaptor.forClass(Scope.class);
+
+        when(scopeRepository.findById(scopeId)).thenReturn(Maybe.just(toUpdate));
+        when(scopeRepository.update(argument.capture())).thenReturn(Single.just(new Scope()));
+        when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
+
+        TestObserver testObserver = scopeService.update(DOMAIN,scopeId, updateScope).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        verify(scopeRepository, times(1)).update(any(Scope.class));
+        assertNotNull(argument.getValue());
+        assertEquals("name",argument.getValue().getName());
+        assertNull(argument.getValue().getDescription());
+        assertFalse(argument.getValue().isDiscovery());
+    }
+
+    @Test
+    public void shouldUpdate_nonSystemScope_discoveryNotReplaced() {
+        UpdateScope updateScope = new UpdateScope();
+        updateScope.setName("name");
+
+        final String scopeId = "toUpdateId";
+
+        Scope toUpdate = new Scope();
+        toUpdate.setId(scopeId);
+        toUpdate.setSystem(false);
+        toUpdate.setDiscovery(true);
+        toUpdate.setName("oldName");
+        toUpdate.setDescription("oldDescription");
+
+        ArgumentCaptor<Scope> argument = ArgumentCaptor.forClass(Scope.class);
+
+        when(scopeRepository.findById(scopeId)).thenReturn(Maybe.just(toUpdate));
+        when(scopeRepository.update(argument.capture())).thenReturn(Single.just(new Scope()));
+        when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
+
+        TestObserver testObserver = scopeService.update(DOMAIN,scopeId, updateScope).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        verify(scopeRepository, times(1)).update(any(Scope.class));
+        assertNotNull(argument.getValue());
+        assertEquals("name",argument.getValue().getName());
+        assertNull(argument.getValue().getDescription());
+        assertTrue(argument.getValue().isDiscovery());
+    }
+
+    @Test
+    public void shouldUpdate_nonSystemScope_discoveryReplaced() {
+        UpdateScope updateScope = new UpdateScope();
+        updateScope.setDiscovery(true);
+        updateScope.setName("name");
+
+        final String scopeId = "toUpdateId";
+
+        Scope toUpdate = new Scope();
+        toUpdate.setId(scopeId);
+        toUpdate.setSystem(false);
+        toUpdate.setDiscovery(false);
+        toUpdate.setName("oldName");
+        toUpdate.setDescription("oldDescription");
+
+        ArgumentCaptor<Scope> argument = ArgumentCaptor.forClass(Scope.class);
+
+        when(scopeRepository.findById(scopeId)).thenReturn(Maybe.just(toUpdate));
+        when(scopeRepository.update(argument.capture())).thenReturn(Single.just(new Scope()));
+        when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
+
+        TestObserver testObserver = scopeService.update(DOMAIN,scopeId, updateScope).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        verify(scopeRepository, times(1)).update(any(Scope.class));
+        assertNotNull(argument.getValue());
+        assertEquals("name",argument.getValue().getName());
+        assertNull(argument.getValue().getDescription());
+        assertTrue(argument.getValue().isDiscovery());
+    }
+
+    @Test
+    public void shouldNotUpdate() {
+        Scope toUpdate = new Scope();
+        toUpdate.setId("toUpdateId");
+
+        when(scopeRepository.findById("toUpdateId")).thenReturn(Maybe.error(TechnicalException::new));
+
+        TestObserver testObserver = scopeService.update(DOMAIN,"toUpdateId", new UpdateScope()).test();
+
+        testObserver.assertError(TechnicalManagementException.class);
+        testObserver.assertNotComplete();
+    }
+
+    @Test
+    public void shouldUpdateSystemScope() {
+        UpdateSystemScope updateScope = new UpdateSystemScope();
+        updateScope.setDiscovery(true);
+        updateScope.setName("name");
+
+        final String scopeId = "toUpdateId";
+
+        Scope toUpdate = new Scope();
+        toUpdate.setId(scopeId);
+        toUpdate.setSystem(true);
+        toUpdate.setDiscovery(false);
+        toUpdate.setName("oldName");
+        toUpdate.setDescription("oldDescription");
+
+        ArgumentCaptor<Scope> argument = ArgumentCaptor.forClass(Scope.class);
+
+        when(scopeRepository.findById(scopeId)).thenReturn(Maybe.just(toUpdate));
+        when(scopeRepository.update(argument.capture())).thenReturn(Single.just(new Scope()));
+        when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
+
+        TestObserver testObserver = scopeService.update(DOMAIN,scopeId, updateScope).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        verify(scopeRepository, times(1)).update(any(Scope.class));
+        assertNotNull(argument.getValue());
+        assertEquals("name",argument.getValue().getName());
+        assertNull(argument.getValue().getDescription());
+        assertTrue(argument.getValue().isDiscovery());
+    }
+
+    @Test
+    public void shouldNotUpdateSystemScope() {
+        Scope toUpdate = new Scope();
+        toUpdate.setId("toUpdateId");
+
+        when(scopeRepository.findById("toUpdateId")).thenReturn(Maybe.error(TechnicalException::new));
+
+        TestObserver testObserver = scopeService.update(DOMAIN,"toUpdateId", new UpdateSystemScope()).test();
+
+        testObserver.assertError(TechnicalManagementException.class);
+        testObserver.assertNotComplete();
+    }
+
     @Test
     public void shouldDelete_notExistingScope() {
         when(scopeRepository.findById("my-scope")).thenReturn(Maybe.empty());