Repository: gravitee-io/graviteeio-access-management
Pull request #680 (2019-11-14 19:29:19)

feat(login): deny access for disabled users with current session
closes gravitee-io/issues#2563


Patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-core/src/main/java/io/gravitee/am/gateway/core/event/Event.java b/gravitee-am-gateway/gravitee-am-gateway-core/src/main/java/io/gravitee/am/gateway/core/event/Event.java
index 20f4201dd4..df48f082b0 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-core/src/main/java/io/gravitee/am/gateway/core/event/Event.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-core/src/main/java/io/gravitee/am/gateway/core/event/Event.java
@@ -57,6 +57,9 @@ public abstract class Event {
             case POLICY:
                 event1 =  PolicyEvent.actionOf(event.getPayload().getAction());
                 break;
+            case USER:
+                event1 =  UserEvent.actionOf(event.getPayload().getAction());
+                break;
         }
 
         return event1;
diff --git a/gravitee-am-gateway/gravitee-am-gateway-core/src/main/java/io/gravitee/am/gateway/core/event/UserEvent.java b/gravitee-am-gateway/gravitee-am-gateway-core/src/main/java/io/gravitee/am/gateway/core/event/UserEvent.java
new file mode 100644
index 0000000000..1414e8a3f0
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-core/src/main/java/io/gravitee/am/gateway/core/event/UserEvent.java
@@ -0,0 +1,45 @@
+/**
+ * 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.core.event;
+
+import io.gravitee.am.model.common.event.Action;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public enum UserEvent {
+
+    DEPLOY,
+    UPDATE,
+    UNDEPLOY;
+
+    public static UserEvent actionOf(Action action) {
+        UserEvent roleEvent = null;
+        switch (action) {
+            case CREATE:
+                roleEvent = UserEvent.DEPLOY;
+                break;
+            case UPDATE:
+                roleEvent = UserEvent.UPDATE;
+                break;
+            case DELETE:
+                roleEvent = UserEvent.UNDEPLOY;
+                break;
+        }
+        return roleEvent;
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/spring/CommonConfiguration.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/spring/CommonConfiguration.java
index 7bfde5c215..5b319cbc3a 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/spring/CommonConfiguration.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/spring/CommonConfiguration.java
@@ -17,12 +17,12 @@ package io.gravitee.am.gateway.handler.common.spring;
 
 import io.gravitee.am.gateway.handler.common.audit.AuditReporterManager;
 import io.gravitee.am.gateway.handler.common.audit.impl.AuditReporterManagerImpl;
-import io.gravitee.am.gateway.handler.common.auth.UserAuthenticationService;
 import io.gravitee.am.gateway.handler.common.auth.UserAuthenticationManager;
+import io.gravitee.am.gateway.handler.common.auth.UserAuthenticationService;
 import io.gravitee.am.gateway.handler.common.auth.idp.IdentityProviderManager;
 import io.gravitee.am.gateway.handler.common.auth.idp.impl.IdentityProviderManagerImpl;
-import io.gravitee.am.gateway.handler.common.auth.impl.UserAuthenticationServiceImpl;
 import io.gravitee.am.gateway.handler.common.auth.impl.UserAuthenticationManagerImpl;
+import io.gravitee.am.gateway.handler.common.auth.impl.UserAuthenticationServiceImpl;
 import io.gravitee.am.gateway.handler.common.authentication.listener.AuthenticationEventListener;
 import io.gravitee.am.gateway.handler.common.certificate.CertificateManager;
 import io.gravitee.am.gateway.handler.common.certificate.CertificateProviderManager;
@@ -43,7 +43,9 @@ import io.gravitee.am.gateway.handler.common.oauth2.impl.IntrospectionTokenServi
 import io.gravitee.am.gateway.handler.common.policy.PolicyManager;
 import io.gravitee.am.gateway.handler.common.policy.impl.PolicyManagerImpl;
 import io.gravitee.am.gateway.handler.common.spring.web.WebConfiguration;
+import io.gravitee.am.gateway.handler.common.user.UserManager;
 import io.gravitee.am.gateway.handler.common.user.UserService;
+import io.gravitee.am.gateway.handler.common.user.impl.UserManagerImpl;
 import io.gravitee.am.gateway.handler.common.user.impl.UserServiceImpl;
 import io.gravitee.am.gateway.handler.common.vertx.web.auth.provider.OAuth2AuthProvider;
 import io.gravitee.am.gateway.handler.common.vertx.web.auth.provider.UserAuthProvider;
@@ -180,4 +182,9 @@ public class CommonConfiguration {
     public UserService userService() {
         return new UserServiceImpl();
     }
+
+    @Bean
+    public UserManager userManager() {
+        return new UserManagerImpl();
+    }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/spring/web/WebConfiguration.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/spring/web/WebConfiguration.java
index 0bc6d78b53..92304a8fd4 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/spring/web/WebConfiguration.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/spring/web/WebConfiguration.java
@@ -33,6 +33,9 @@ public class WebConfiguration {
     @Bean
     public SessionHandlerFactory sessionHandler() { return new SessionHandlerFactory(); }
 
+    @Bean
+    public SSOSessionHandlerFactory ssoSessionHandler() { return new SSOSessionHandlerFactory(); }
+
     @Bean
     public CookieHandlerFactory cookieHandler() {
         return new CookieHandlerFactory();
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/user/UserManager.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/user/UserManager.java
new file mode 100644
index 0000000000..afe608f81d
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/user/UserManager.java
@@ -0,0 +1,29 @@
+/**
+ * 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.common.user;
+
+import io.gravitee.am.model.User;
+import io.gravitee.common.service.Service;
+import io.reactivex.Maybe;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface UserManager extends Service {
+
+    Maybe<User> get(String userId);
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/user/impl/UserManagerImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/user/impl/UserManagerImpl.java
new file mode 100644
index 0000000000..5b0f98b682
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/user/impl/UserManagerImpl.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.common.user.impl;
+
+import io.gravitee.am.gateway.core.event.EventManager;
+import io.gravitee.am.gateway.core.event.UserEvent;
+import io.gravitee.am.gateway.handler.common.user.UserManager;
+import io.gravitee.am.gateway.handler.common.user.UserService;
+import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.User;
+import io.gravitee.am.model.common.event.Payload;
+import io.gravitee.common.event.Event;
+import io.gravitee.common.event.EventListener;
+import io.gravitee.common.service.AbstractService;
+import io.reactivex.Maybe;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class UserManagerImpl extends AbstractService implements UserManager, EventListener<UserEvent, Payload> {
+
+    private static final Logger logger = LoggerFactory.getLogger(UserManagerImpl.class);
+
+    @Autowired
+    private Domain domain;
+
+    @Autowired
+    private EventManager eventManager;
+
+    @Autowired
+    private UserService userService;
+
+    private ConcurrentMap<String, User> users = new ConcurrentHashMap<>();
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+
+        logger.info("Register event listener for user events for domain {}", domain.getName());
+        eventManager.subscribeForEvents(this, UserEvent.class, domain.getId());
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        super.doStop();
+
+        logger.info("Dispose event listener for user events for domain {}", domain.getName());
+        eventManager.unsubscribeForEvents(this, UserEvent.class, domain.getId());
+        users.clear();
+    }
+
+    @Override
+    public void onEvent(Event<UserEvent, Payload> event) {
+        if (domain.getId().equals(event.content().getDomain())) {
+            switch (event.type()) {
+                case DEPLOY:
+                case UPDATE:
+                    updateUser(event.content().getId(), event.type());
+                    break;
+                case UNDEPLOY:
+                    removeUser(event.content().getId());
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public Maybe<User> get(String userId) {
+        return users.get(userId) != null ? Maybe.just(users.get(userId)) : Maybe.empty();
+    }
+
+    private void updateUser(String userId, UserEvent userEvent) {
+        final String eventType = userEvent.toString().toLowerCase();
+        logger.info("Domain {} has received {} user event for {}", domain.getName(), eventType, userId);
+        userService.findById(userId)
+                .subscribe(
+                        user -> {
+                            users.put(userId, user);
+                            logger.info("User {} {}d for domain {}", userId, eventType, domain.getName());
+                        },
+                        error -> logger.error("Unable to {} user for domain {}", eventType, domain.getName(), error),
+                        () -> logger.error("No user found with id {}", userId));
+    }
+
+    private void removeUser(String userId) {
+        logger.info("Domain {} has received user event, delete user {}", domain.getName(), userId);
+        users.remove(userId);
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/vertx/web/handler/SSOSessionHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/vertx/web/handler/SSOSessionHandler.java
new file mode 100644
index 0000000000..1a633ad43a
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/vertx/web/handler/SSOSessionHandler.java
@@ -0,0 +1,86 @@
+/**
+ * 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.common.vertx.web.handler;
+
+import io.gravitee.am.common.exception.authentication.AccountDisabledException;
+import io.gravitee.am.gateway.handler.common.user.UserManager;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.reactivex.ext.auth.User;
+import io.vertx.reactivex.ext.web.RoutingContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * SSO Session Handler to check if the user stored in the HTTP session is still "valid" upon the incoming request
+ *
+ * - a user is invalid if he is disabled
+ *
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SSOSessionHandler implements Handler<RoutingContext> {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(SSOSessionHandler.class);
+    private UserManager userManager;
+
+    SSOSessionHandler(UserManager userManager) {
+        this.userManager = userManager;
+    }
+
+    @Override
+    public void handle(RoutingContext context) {
+        // if no user in context, continue
+        if (context.user() == null) {
+            context.next();
+            return;
+        }
+
+        // retrieve end user and check if it's authorized to call the subsequence handlers
+        User authenticatedUser = context.user();
+        io.gravitee.am.model.User endUser = ((io.gravitee.am.gateway.handler.common.vertx.web.auth.user.User) authenticatedUser.getDelegate()).getUser();
+
+        authorize(endUser, h -> {
+            if (h.failed()) {
+                Throwable cause = h.cause();
+                LOGGER.debug("An error occurs while checking SSO Session upon the current user : {}", endUser.getId(), cause);
+                if (cause instanceof AccountDisabledException) {
+                    // user has been disabled, invalidate session
+                    context.clearUser();
+                    context.session().destroy();
+                }
+            }
+            context.next();
+        });
+
+    }
+
+    private void authorize(io.gravitee.am.model.User user, Handler<AsyncResult<Void>> handler) {
+        userManager.get(user.getId())
+                .subscribe(
+                        user1 -> {
+                            // if user is disabled, throw exception
+                            if (!user1.isEnabled()) {
+                                handler.handle(Future.failedFuture(new AccountDisabledException(user1.getId())));
+                                return;
+                            }
+                            handler.handle(Future.succeededFuture());
+                        },
+                        error -> handler.handle(Future.failedFuture(error)),
+                        () -> handler.handle(Future.succeededFuture()));
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/vertx/web/handler/SSOSessionHandlerFactory.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/vertx/web/handler/SSOSessionHandlerFactory.java
new file mode 100644
index 0000000000..fc049d78cd
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/vertx/web/handler/SSOSessionHandlerFactory.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.common.vertx.web.handler;
+
+import io.gravitee.am.gateway.handler.common.user.UserManager;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SSOSessionHandlerFactory implements FactoryBean<SSOSessionHandler> {
+
+    @Autowired
+    private UserManager userManager;
+
+    @Override
+    public SSOSessionHandler getObject() {
+        return new SSOSessionHandler(userManager);
+    }
+
+    @Override
+    public Class<?> getObjectType() {
+        return SSOSessionHandler.class;
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/SecurityDomainRouterFactory.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/SecurityDomainRouterFactory.java
index 40d5b00279..b659212f60 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/SecurityDomainRouterFactory.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/SecurityDomainRouterFactory.java
@@ -21,6 +21,7 @@ import io.gravitee.am.gateway.handler.common.authentication.listener.Authenticat
 import io.gravitee.am.gateway.handler.common.certificate.CertificateManager;
 import io.gravitee.am.gateway.handler.common.client.ClientSyncService;
 import io.gravitee.am.gateway.handler.common.policy.PolicyManager;
+import io.gravitee.am.gateway.handler.common.user.UserManager;
 import io.gravitee.am.gateway.handler.email.EmailManager;
 import io.gravitee.am.gateway.handler.form.FormManager;
 import io.gravitee.am.gateway.handler.spring.HandlerConfiguration;
@@ -100,6 +101,7 @@ public class SecurityDomainRouterFactory {
         components.add(AuditReporterManager.class);
         components.add(PolicyManager.class);
         components.add(AuthenticationEventListener.class);
+        components.add(UserManager.class);
 
         components.forEach(componentClass -> {
             LifecycleComponent lifecyclecomponent = applicationContext.getBean(componentClass);
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/vertx/VertxSecurityDomainHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/vertx/VertxSecurityDomainHandler.java
index a909694e2e..7cf3e55692 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/vertx/VertxSecurityDomainHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/vertx/VertxSecurityDomainHandler.java
@@ -22,6 +22,7 @@ import io.gravitee.am.gateway.handler.common.authentication.listener.Authenticat
 import io.gravitee.am.gateway.handler.common.certificate.CertificateManager;
 import io.gravitee.am.gateway.handler.common.client.ClientSyncService;
 import io.gravitee.am.gateway.handler.common.policy.PolicyManager;
+import io.gravitee.am.gateway.handler.common.user.UserManager;
 import io.gravitee.am.gateway.handler.email.EmailManager;
 import io.gravitee.am.gateway.handler.form.FormManager;
 import io.gravitee.am.gateway.handler.root.RootProvider;
@@ -142,6 +143,7 @@ public class VertxSecurityDomainHandler extends AbstractService<VertxSecurityDom
         components.add(AuditReporterManager.class);
         components.add(PolicyManager.class);
         components.add(AuthenticationEventListener.class);
+        components.add(UserManager.class);
 
         components.forEach(componentClass -> {
             LifecycleComponent lifecyclecomponent = applicationContext.getBean(componentClass);
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java
index e95f939b85..1b821551a8 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java
@@ -22,6 +22,7 @@ import io.gravitee.am.gateway.handler.common.vertx.web.auth.handler.RedirectAuth
 import io.gravitee.am.gateway.handler.common.vertx.web.auth.provider.UserAuthProvider;
 import io.gravitee.am.gateway.handler.common.vertx.web.endpoint.ErrorEndpoint;
 import io.gravitee.am.gateway.handler.common.vertx.web.handler.PolicyChainHandler;
+import io.gravitee.am.gateway.handler.common.vertx.web.handler.SSOSessionHandler;
 import io.gravitee.am.gateway.handler.oauth2.resources.auth.handler.ClientAssertionAuthHandler;
 import io.gravitee.am.gateway.handler.oauth2.resources.auth.handler.ClientBasicAuthHandler;
 import io.gravitee.am.gateway.handler.oauth2.resources.auth.handler.ClientCredentialsAuthHandler;
@@ -120,6 +121,9 @@ public class OAuth2Provider extends AbstractService<ProtocolProvider> implements
     @Autowired
     private SessionHandler sessionHandler;
 
+    @Autowired
+    private SSOSessionHandler ssoSessionHandler;
+
     @Autowired
     private CookieHandler cookieHandler;
 
@@ -268,11 +272,13 @@ public class OAuth2Provider extends AbstractService<ProtocolProvider> implements
         router
                 .route("/authorize")
                 .handler(cookieHandler)
-                .handler(sessionHandler);
+                .handler(sessionHandler)
+                .handler(ssoSessionHandler);
         router
                 .route("/confirm_access")
                 .handler(cookieHandler)
-                .handler(sessionHandler);
+                .handler(sessionHandler)
+                .handler(ssoSessionHandler);
     }
 
     private void csrfHandler(Router router) {
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AbstractAuthorizationEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AbstractAuthorizationEndpoint.java
index a512239927..ecf47b8501 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AbstractAuthorizationEndpoint.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AbstractAuthorizationEndpoint.java
@@ -32,13 +32,10 @@ public abstract class AbstractAuthorizationEndpoint {
         AuthorizationRequest authorizationRequest = routingContext.session().get(OAuth2Constants.AUTHORIZATION_REQUEST);
         // we have the authorization request in session if we come from the approval user page
         if (authorizationRequest != null) {
-            // remove OAuth2Constants.AUTHORIZATION_REQUEST session value
-            // should not be used after this step
-            routingContext.session().remove(OAuth2Constants.AUTHORIZATION_REQUEST);
             return authorizationRequest;
         }
 
-        // the initial request failed for some reasons, we have the required request parameters to re-create the authorize request
+        // if none, we have the required request parameters to re-create the authorize request
         return createAuthorizationRequest(routingContext);
     }
 
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java
index 14a44ec2af..b2c90cb4c4 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java
@@ -20,9 +20,9 @@ import io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest;
 import io.gravitee.am.gateway.handler.oauth2.exception.AccessDeniedException;
 import io.gravitee.am.gateway.handler.oauth2.exception.InteractionRequiredException;
 import io.gravitee.am.gateway.handler.oauth2.exception.ServerErrorException;
-import io.gravitee.am.gateway.handler.oidc.service.flow.Flow;
 import io.gravitee.am.gateway.handler.oauth2.service.request.AuthorizationRequest;
 import io.gravitee.am.gateway.handler.oauth2.service.utils.OAuth2Constants;
+import io.gravitee.am.gateway.handler.oidc.service.flow.Flow;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
 import io.gravitee.common.http.HttpHeaders;
@@ -59,7 +59,7 @@ public class AuthorizationEndpoint extends AbstractAuthorizationEndpoint impleme
 
     @Override
     public void handle(RoutingContext context) {
-        AuthorizationRequest request = createAuthorizationRequest(context);
+        AuthorizationRequest request = resolveInitialAuthorizeRequest(context);
 
         // The authorization server authenticates the resource owner and obtains
         // an authorization decision (by asking the resource owner or by establishing approval via other means).
@@ -77,6 +77,8 @@ public class AuthorizationEndpoint extends AbstractAuthorizationEndpoint impleme
         flow.run(request, client, endUser)
                 .subscribe(authorizationResponse -> {
                     try {
+                        // final step of the authorization flow, we can clean the session and redirect the user
+                        cleanSession(context);
                         doRedirect(context.response(), authorizationResponse.buildRedirectUri());
                     } catch (Exception e) {
                         logger.error("Unable to redirect to client redirect_uri", e);
@@ -107,4 +109,8 @@ public class AuthorizationEndpoint extends AbstractAuthorizationEndpoint impleme
     private void doRedirect(HttpServerResponse response, String url) {
         response.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
     }
+
+    private void cleanSession(RoutingContext context) {
+        context.session().remove(OAuth2Constants.AUTHORIZATION_REQUEST);
+    }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationFailureEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationFailureEndpoint.java
index 376eb2235f..0e18fa41f5 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationFailureEndpoint.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationFailureEndpoint.java
@@ -15,17 +15,19 @@
  */
 package io.gravitee.am.gateway.handler.oauth2.resources.endpoint.authorization;
 
-import io.gravitee.am.common.oauth2.Parameters;
 import io.gravitee.am.common.exception.oauth2.OAuth2Exception;
+import io.gravitee.am.common.oauth2.Parameters;
+import io.gravitee.am.common.web.UriBuilder;
 import io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest;
 import io.gravitee.am.gateway.handler.oauth2.exception.RedirectMismatchException;
 import io.gravitee.am.gateway.handler.oauth2.service.request.AuthorizationRequest;
+import io.gravitee.am.gateway.handler.oauth2.service.utils.OAuth2Constants;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
-import io.gravitee.am.common.web.UriBuilder;
 import io.gravitee.common.http.HttpHeaders;
 import io.gravitee.common.http.HttpStatusCode;
 import io.vertx.core.Handler;
+import io.vertx.ext.web.handler.impl.HttpStatusException;
 import io.vertx.reactivex.core.http.HttpServerResponse;
 import io.vertx.reactivex.ext.web.RoutingContext;
 import org.slf4j.Logger;
@@ -86,6 +88,11 @@ public class AuthorizationFailureEndpoint extends AbstractAuthorizationEndpoint
                     }
                     // redirect user
                     doRedirect(routingContext.response(), buildRedirectUri(oAuth2Exception, request));
+                } else if (throwable instanceof HttpStatusException) {
+                    routingContext
+                            .response()
+                            .setStatusCode(((HttpStatusException) throwable).getStatusCode())
+                            .end();
                 } else {
                     logger.error("An exception occurs while handling authorization request", throwable);
                     if (routingContext.statusCode() != -1) {
@@ -103,6 +110,9 @@ public class AuthorizationFailureEndpoint extends AbstractAuthorizationEndpoint
             } catch (Exception e) {
                 logger.error("Unable to handle authorization error response", e);
                 doRedirect(routingContext.response(),  "/" + domain.getPath() + "/oauth/error");
+            } finally {
+                // clean session
+                cleanSession(routingContext);
             }
         }
     }
@@ -111,6 +121,10 @@ public class AuthorizationFailureEndpoint extends AbstractAuthorizationEndpoint
         response.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
     }
 
+    private void cleanSession(RoutingContext context) {
+        context.session().remove(OAuth2Constants.AUTHORIZATION_REQUEST);
+    }
+
     private String buildRedirectUri(OAuth2Exception oAuth2Exception, AuthorizationRequest authorizationRequest) throws URISyntaxException {
         // prepare query
         Map<String, String> query = new LinkedHashMap<>();
diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/common/event/Type.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/common/event/Type.java
index a6c14df166..9aeb13d680 100644
--- a/gravitee-am-model/src/main/java/io/gravitee/am/model/common/event/Type.java
+++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/common/event/Type.java
@@ -31,5 +31,6 @@ public enum Type {
     FORM,
     EMAIL,
     REPORTER,
-    POLICY
+    POLICY,
+    USER
 }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
index a4103078ce..446d6e198b 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
@@ -18,7 +18,12 @@ package io.gravitee.am.service.impl;
 import io.gravitee.am.common.utils.RandomString;
 import io.gravitee.am.model.User;
 import io.gravitee.am.model.common.Page;
+import io.gravitee.am.model.common.event.Action;
+import io.gravitee.am.model.common.event.Event;
+import io.gravitee.am.model.common.event.Payload;
+import io.gravitee.am.model.common.event.Type;
 import io.gravitee.am.repository.management.api.UserRepository;
+import io.gravitee.am.service.EventService;
 import io.gravitee.am.service.UserService;
 import io.gravitee.am.service.exception.AbstractManagementException;
 import io.gravitee.am.service.exception.TechnicalManagementException;
@@ -51,6 +56,9 @@ public class UserServiceImpl implements UserService {
     @Autowired
     private UserRepository userRepository;
 
+    @Autowired
+    private EventService eventService;
+
     @Override
     public Single<Set<User>> findByDomain(String domain) {
         LOGGER.debug("Find users by domain: {}", domain);
@@ -181,6 +189,11 @@ public class UserServiceImpl implements UserService {
                         return userRepository.create(user);
                     }
                 })
+                .flatMap(user -> {
+                    // create event for sync process
+                    Event event = new Event(Type.USER, new Payload(user.getId(), user.getDomain(), Action.CREATE));
+                    return eventService.create(event).flatMap(__ -> Single.just(user));
+                })
                 .onErrorResumeNext(ex -> {
                     if (ex instanceof AbstractManagementException) {
                         return Single.error(ex);
@@ -197,6 +210,11 @@ public class UserServiceImpl implements UserService {
         user.setCreatedAt(new Date());
         user.setUpdatedAt(user.getCreatedAt());
         return userRepository.create(user)
+                .flatMap(user1 -> {
+                    // create event for sync process
+                    Event event = new Event(Type.USER, new Payload(user1.getId(), user1.getDomain(), Action.CREATE));
+                    return eventService.create(event).flatMap(__ -> Single.just(user1));
+                })
                 .onErrorResumeNext(ex -> {
                     if (ex instanceof AbstractManagementException) {
                         return Single.error(ex);
@@ -225,6 +243,11 @@ public class UserServiceImpl implements UserService {
                     oldUser.setAdditionalInformation(updateUser.getAdditionalInformation());
                     return userRepository.update(oldUser);
                 })
+                .flatMap(user -> {
+                    // create event for sync process
+                    Event event = new Event(Type.USER, new Payload(user.getId(), user.getDomain(), Action.UPDATE));
+                    return eventService.create(event).flatMap(__ -> Single.just(user));
+                })
                 .onErrorResumeNext(ex -> {
                     if (ex instanceof AbstractManagementException) {
                         return Single.error(ex);
@@ -241,6 +264,11 @@ public class UserServiceImpl implements UserService {
         // updated date
         user.setUpdatedAt(new Date());
         return userRepository.update(user)
+                .flatMap(user1 -> {
+                    // create event for sync process
+                    Event event = new Event(Type.USER, new Payload(user1.getId(), user1.getDomain(), Action.UPDATE));
+                    return eventService.create(event).flatMap(__ -> Single.just(user1));
+                })
                 .onErrorResumeNext(ex -> {
                     if (ex instanceof AbstractManagementException) {
                         return Single.error(ex);
@@ -256,7 +284,11 @@ public class UserServiceImpl implements UserService {
 
         return userRepository.findById(userId)
                 .switchIfEmpty(Maybe.error(new UserNotFoundException(userId)))
-                .flatMapCompletable(user -> userRepository.delete(userId))
+                .flatMapCompletable(user -> {
+                        // create event for sync process
+                        Event event = new Event(Type.USER, new Payload(user.getId(), user.getDomain(), Action.DELETE));
+                        return userRepository.delete(userId).andThen(eventService.create(event)).toCompletable();
+                })
                 .onErrorResumeNext(ex -> {
                     if (ex instanceof AbstractManagementException) {
                         return Completable.error(ex);
diff --git a/postman/collections/graviteeio-am-login-collection.json b/postman/collections/graviteeio-am-login-collection.json
index b90dd58165..784fed1cca 100644
--- a/postman/collections/graviteeio-am-login-collection.json
+++ b/postman/collections/graviteeio-am-login-collection.json
@@ -1,6 +1,6 @@
 {
 	"info": {
-		"_postman_id": "6410e041-7875-4278-b114-e4d70957dbbd",
+		"_postman_id": "441ad7e8-bcba-47e9-8978-78b99b1303a0",
 		"name": "Gravitee.io - AM - Login",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
 	},
@@ -151,7 +151,7 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n  \"external\": false,\n  \"type\": \"inline-am-idp\",\n  \"configuration\": \"{\\\"users\\\":[{\\\"firstname\\\":\\\"my-user\\\",\\\"lastname\\\":\\\"my-user-lastname\\\",\\\"username\\\":\\\"user\\\",\\\"password\\\":\\\"password\\\"},{\\\"firstname\\\":\\\"Jensen\\\",\\\"lastname\\\":\\\"Barbara\\\",\\\"username\\\":\\\"jensen.barbara\\\",\\\"email\\\":\\\"jensen.barbara@mail.com\\\",\\\"password\\\":\\\"password\\\"}]}\",\n  \"name\": \"inmemory\"\n}"
+							"raw": "{\n  \"external\": false,\n  \"type\": \"inline-am-idp\",\n  \"configuration\": \"{\\\"users\\\":[{\\\"firstname\\\":\\\"my-user\\\",\\\"lastname\\\":\\\"my-user-lastname\\\",\\\"username\\\":\\\"user\\\",\\\"password\\\":\\\"password\\\"}]}\",\n  \"name\": \"inmemory\"\n}"
 						},
 						"url": {
 							"raw": "{{management_url}}/management/domains/{{domain}}/identities",
@@ -310,8 +310,626 @@
 						}
 					},
 					"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 discovery endpoints\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"    ",
+									"    pm.expect(body).to.have.property(\"authorization_endpoint\");",
+									"    pm.expect(body).to.have.property(\"token_endpoint\");",
+									"    pm.expect(body).to.have.property(\"revocation_endpoint\");",
+									"    pm.expect(body).to.have.property(\"userinfo_endpoint\");",
+									"    pm.expect(body).to.have.property(\"registration_endpoint\");",
+									"    pm.expect(body).to.have.property(\"end_session_endpoint\");",
+									"    pm.expect(body).to.have.property(\"introspection_endpoint\");",
+									"",
+									"    pm.environment.set('authorizationEndpoint', body.authorization_endpoint);",
+									"    pm.environment.set('tokenEndpoint', body.token_endpoint);",
+									"    pm.environment.set('userinfoEndpoint', body.userinfo_endpoint);",
+									"    pm.environment.set('revocationEndpoint', body.revocation_endpoint);",
+									"    pm.environment.set('registrationEndpoint', body.registration_endpoint);",
+									"    pm.environment.set('logoutEndpoint', body.end_session_endpoint);",
+									"    pm.environment.set('introspectionEndpoint', body.introspection_endpoint);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{gateway_url}}/{{domain}}/oidc/.well-known/openid-configuration",
+							"host": [
+								"{{gateway_url}}"
+							],
+							"path": [
+								"{{domain}}",
+								"oidc",
+								".well-known",
+								"openid-configuration"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Account Disabled",
+			"item": [
+				{
+					"name": "Create user",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "ac86c3c9-c0a7-4616-b7cf-6847a7f48801",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});",
+									"",
+									"pm.test(\"UM - create user\", function () {",
+									"    var jsonData = pm.response.json();",
+									"    let domain = pm.environment.get(\"domain\");",
+									"    ",
+									"    pm.expect(jsonData).to.have.property('id');",
+									"    pm.expect(jsonData.internal).to.eql(true);",
+									"    pm.expect(jsonData.enabled).to.eql(true);",
+									"    pm.expect(jsonData.preRegistration).to.eql(false);",
+									"    pm.expect(jsonData.registrationCompleted).to.eql(true);",
+									"    pm.expect(jsonData.source).to.eql('default-idp-'+domain)",
+									"    pm.environment.set('userUM', jsonData.id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}",
+								"type": "text"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"value": "application/json",
+								"type": "text"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\"firstName\": \"Jensen\",\n\t\"lastName\": \"Barbara\",\n\t\"username\": \"jensen.barbara\",\n\t\"email\": \"jensen.barbara@mail.com\",\n\t\"password\": \"password\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/users",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"users"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Configure client",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "c37cba72-91c2-4850-80cd-6fd3908e406e",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"// wait for sync process",
+									"setTimeout(function(){}, 5000);"
+								],
+								"type": "text/javascript"
+							}
+						},
+						{
+							"listen": "prerequest",
+							"script": {
+								"id": "b665041c-4138-4807-944d-1e278c44cfcb",
+								"exec": [
+									"let domain = pm.environment.get(\"domain\");",
+									"let identityProvider = 'default-idp-'+domain;",
+									"pm.environment.set(\"identityProvider\",identityProvider)"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"identities\" : [\"{{idp.inmemory}}\", \"{{identityProvider}}\"]\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{client}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"clients",
+								"{{client}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Initiate Login Flow",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "fb56a849-b090-4e80-9c8c-eca9f4d72e85",
+								"exec": [
+									"pm.test(\"Status code is 302\", function () {",
+									"    pm.response.to.have.status(302);",
+									"});",
+									"",
+									"pm.test(\"Should be redirected\", function () {",
+									"    pm.response.to.be.redirection;",
+									"    pm.response.to.have.header('Location');",
+									"});",
+									"",
+									"pm.test(\"Should be a redirection to login page\", function() {",
+									"    var location = postman.getResponseHeader('Location');",
+									"    let domain = pm.environment.get(\"domain\");",
+									"    ",
+									"    tests['Redirect to login page with client_id'] = location === pm.environment.get('gateway_url') + '/' + domain + '/login?client_id=my-client';",
+									"    ",
+									"    pm.environment.set('redirection', postman.getResponseHeader(\"Location\"));",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						},
+						{
+							"listen": "prerequest",
+							"script": {
+								"id": "904f0113-b2b9-4645-9511-48bb10791506",
+								"exec": [
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{gateway_url}}/login/oauth/authorize/?response_type=code&client_id=my-client&redirect_uri=http://localhost:4000/&state=1234-5678-9876",
+							"host": [
+								"{{gateway_url}}"
+							],
+							"path": [
+								"login",
+								"oauth",
+								"authorize",
+								""
+							],
+							"query": [
+								{
+									"key": "response_type",
+									"value": "code"
+								},
+								{
+									"key": "client_id",
+									"value": "my-client"
+								},
+								{
+									"key": "redirect_uri",
+									"value": "http://localhost:4000/"
+								},
+								{
+									"key": "state",
+									"value": "1234-5678-9876"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Redirect to login form",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "67df9bf8-89a2-4c95-9728-097aad6d2be4",
+								"type": "text/javascript",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Should be ok\", function () {",
+									"    pm.response.to.be.ok;",
+									"    ",
+									"    // Extract the XSRF token to send it with the next request.",
+									"    var responseHTML = cheerio(pm.response.text());",
+									"    var xsrfToken = responseHTML.find('[name=\"X-XSRF-TOKEN\"]').val();",
+									"    pm.environment.set('xsrf', xsrfToken);",
+									"});"
+								]
+							}
+						},
+						{
+							"listen": "prerequest",
+							"script": {
+								"id": "904f0113-b2b9-4645-9511-48bb10791506",
+								"type": "text/javascript",
+								"exec": [
+									""
+								]
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{redirection}}",
+							"host": [
+								"{{redirection}}"
+							]
+						},
+						"description": "The client does not have a redirect_uri define"
+					},
+					"response": []
+				},
+				{
+					"name": "Post login form",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "4afec9f2-a5ba-4647-9a78-6d74cb9c157c",
+								"exec": [
+									"pm.test(\"Status code is 302\", function () {",
+									"    pm.response.to.have.status(302);",
+									"});",
+									"",
+									"pm.test(\"Should be redirected to app with valid account\", function () {",
+									"    pm.response.to.be.redirection;",
+									"    pm.response.to.have.header('Location');",
+									"",
+									"    var location = postman.getResponseHeader(\"Location\");",
+									"    pm.environment.set('redirection', location);",
+									"    tests['Redirect to app page'] = location.includes('?response_type=code');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						},
+						{
+							"listen": "prerequest",
+							"script": {
+								"id": "904f0113-b2b9-4645-9511-48bb10791506",
+								"exec": [
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/x-www-form-urlencoded"
+							}
+						],
+						"body": {
+							"mode": "urlencoded",
+							"urlencoded": [
+								{
+									"key": "X-XSRF-TOKEN",
+									"value": "{{xsrf}}",
+									"type": "text"
+								},
+								{
+									"key": "client_id",
+									"value": "my-client",
+									"type": "text"
+								},
+								{
+									"key": "username",
+									"value": "jensen.barbara",
+									"type": "text"
+								},
+								{
+									"key": "password",
+									"value": "password",
+									"type": "text"
+								}
+							]
+						},
+						"url": {
+							"raw": "{{gateway_url}}/{{domain}}/login",
+							"host": [
+								"{{gateway_url}}"
+							],
+							"path": [
+								"{{domain}}",
+								"login"
+							]
+						},
+						"description": "The client does not have a redirect_uri define"
+					},
+					"response": []
+				},
+				{
+					"name": "Update user - disable user",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "56ffba4f-b70a-452d-a063-8c3865594fd4",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"UM - update user\", function () {",
+									"    var jsonData = pm.response.json();",
+									"    pm.expect(jsonData.enabled).to.eql(false);",
+									"});",
+									"",
+									"// wait for sync process",
+									"setTimeout(function(){}, 5000);"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PUT",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\"firstName\": \"Jensen\",\n\t\"lastName\": \"Barbara\",\n\t\"email\": \"jensen.barbara@mail.com\",\n\t\"additionalInformation\": {\n\t\t\"profile\":\"https://my.profile.com\"\n\t},\n\t\"enabled\": false\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/users/{{userUM}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"users",
+								"{{userUM}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Initiate Login Flow",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "fb56a849-b090-4e80-9c8c-eca9f4d72e85",
+								"exec": [
+									"pm.test(\"Status code is 302\", function () {",
+									"    pm.response.to.have.status(302);",
+									"});",
+									"",
+									"pm.test(\"Should be redirected\", function () {",
+									"    pm.response.to.be.redirection;",
+									"    pm.response.to.have.header('Location');",
+									"});",
+									"",
+									"pm.test(\"Should be a redirection to login page\", function() {",
+									"    var location = postman.getResponseHeader('Location');",
+									"    let domain = pm.environment.get(\"domain\");",
+									"    ",
+									"    tests['Redirect to login page with client_id'] = location === pm.environment.get('gateway_url') + '/' + domain + '/login?client_id=my-client';",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						},
+						{
+							"listen": "prerequest",
+							"script": {
+								"id": "904f0113-b2b9-4645-9511-48bb10791506",
+								"exec": [
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{gateway_url}}/login/oauth/authorize/?response_type=code&client_id=my-client&redirect_uri=http://localhost:4000/&state=1234-5678-9876",
+							"host": [
+								"{{gateway_url}}"
+							],
+							"path": [
+								"login",
+								"oauth",
+								"authorize",
+								""
+							],
+							"query": [
+								{
+									"key": "response_type",
+									"value": "code"
+								},
+								{
+									"key": "client_id",
+									"value": "my-client"
+								},
+								{
+									"key": "redirect_uri",
+									"value": "http://localhost:4000/"
+								},
+								{
+									"key": "state",
+									"value": "1234-5678-9876"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Logout user",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "ba6b6fb1-32ac-46d5-8e2d-f97961029a05",
+								"exec": [
+									"pm.test(\"Status code is 302\", function () {",
+									"    pm.response.to.have.status(302);",
+									"});",
+									"",
+									"pm.test(\"Should be redirected\", function () {",
+									"    pm.response.to.be.redirection;",
+									"    pm.response.to.have.header('Location');",
+									"    ",
+									"    pm.environment.set('redirection', postman.getResponseHeader(\"Location\"));",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						},
+						{
+							"listen": "prerequest",
+							"script": {
+								"id": "904f0113-b2b9-4645-9511-48bb10791506",
+								"exec": [
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{logoutEndpoint}}",
+							"host": [
+								"{{logoutEndpoint}}"
+							]
+						},
+						"description": "The client does not have a redirect_uri define"
+					},
+					"response": []
+				},
+				{
+					"name": "Delete user",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "56ffba4f-b70a-452d-a063-8c3865594fd4",
+								"exec": [
+									"pm.test(\"Status code is 204\", function () {",
+									"    pm.response.to.have.status(204);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/users/{{userUM}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"users",
+								"{{userUM}}"
+							]
+						}
+					},
+					"response": []
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
 			"name": "Account Locked",
@@ -735,8 +1353,55 @@
 						"description": "The client does not have a redirect_uri define"
 					},
 					"response": []
+				},
+				{
+					"name": "Logout user",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "ba6b6fb1-32ac-46d5-8e2d-f97961029a05",
+								"exec": [
+									"pm.test(\"Status code is 302\", function () {",
+									"    pm.response.to.have.status(302);",
+									"});",
+									"",
+									"pm.test(\"Should be redirected\", function () {",
+									"    pm.response.to.be.redirection;",
+									"    pm.response.to.have.header('Location');",
+									"    ",
+									"    pm.environment.set('redirection', postman.getResponseHeader(\"Location\"));",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						},
+						{
+							"listen": "prerequest",
+							"script": {
+								"id": "904f0113-b2b9-4645-9511-48bb10791506",
+								"exec": [
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{logoutEndpoint}}",
+							"host": [
+								"{{logoutEndpoint}}"
+							]
+						},
+						"description": "The client does not have a redirect_uri define"
+					},
+					"response": []
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
 			"name": "Case insensitive",
@@ -852,7 +1517,8 @@
 					},
 					"response": []
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
 			"name": "Delete domain",
@@ -900,5 +1566,6 @@
 			},
 			"response": []
 		}
-	]
-}
+	],
+	"protocolProfileBehavior": {}
+}
\ No newline at end of file


Test patch:
diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/UserServiceTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/UserServiceTest.java
index 6795093f1f..35c6fea815 100644
--- a/gravitee-am-service/src/test/java/io/gravitee/am/service/UserServiceTest.java
+++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/UserServiceTest.java
@@ -15,11 +15,10 @@
  */
 package io.gravitee.am.service;
 
-import io.gravitee.am.model.Group;
 import io.gravitee.am.model.User;
 import io.gravitee.am.model.common.Page;
+import io.gravitee.am.model.common.event.Event;
 import io.gravitee.am.repository.exceptions.TechnicalException;
-import io.gravitee.am.repository.management.api.GroupRepository;
 import io.gravitee.am.repository.management.api.UserRepository;
 import io.gravitee.am.service.exception.TechnicalManagementException;
 import io.gravitee.am.service.exception.UserAlreadyExistsException;
@@ -38,7 +37,8 @@ import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.runners.MockitoJUnitRunner;
 
-import java.util.*;
+import java.util.Collections;
+import java.util.Set;
 
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.*;
@@ -57,7 +57,7 @@ public class UserServiceTest {
     private UserRepository userRepository;
 
     @Mock
-    private GroupRepository groupRepository;
+    private EventService eventService;
 
     private final static String DOMAIN = "domain1";
 
@@ -174,6 +174,7 @@ public class UserServiceTest {
         when(newUser.getSource()).thenReturn("source");
         when(userRepository.create(any(User.class))).thenReturn(Single.just(new User()));
         when(userRepository.findByDomainAndUsernameAndSource(DOMAIN, newUser.getUsername(), newUser.getSource())).thenReturn(Maybe.empty());
+        when(eventService.create(any())).thenReturn(Single.just(new Event()));
 
         TestObserver testObserver = userService.create(DOMAIN, newUser).test();
         testObserver.awaitTerminalEvent();
@@ -182,6 +183,7 @@ public class UserServiceTest {
         testObserver.assertNoErrors();
 
         verify(userRepository, times(1)).create(any(User.class));
+        verify(eventService, times(1)).create(any());
     }
 
     @Test
@@ -218,6 +220,7 @@ public class UserServiceTest {
         UpdateUser updateUser = Mockito.mock(UpdateUser.class);
         when(userRepository.findById("my-user")).thenReturn(Maybe.just(new User()));
         when(userRepository.update(any(User.class))).thenReturn(Single.just(new User()));
+        when(eventService.create(any())).thenReturn(Single.just(new Event()));
 
         TestObserver testObserver = userService.update(DOMAIN, "my-user", updateUser).test();
         testObserver.awaitTerminalEvent();
@@ -227,6 +230,7 @@ public class UserServiceTest {
 
         verify(userRepository, times(1)).findById("my-user");
         verify(userRepository, times(1)).update(any(User.class));
+        verify(eventService, times(1)).create(any());
     }
 
     @Test
@@ -258,6 +262,7 @@ public class UserServiceTest {
     public void shouldDelete() {
         when(userRepository.findById("my-user")).thenReturn(Maybe.just(new User()));
         when(userRepository.delete("my-user")).thenReturn(Completable.complete());
+        when(eventService.create(any())).thenReturn(Single.just(new Event()));
 
         TestObserver testObserver = userService.delete("my-user").test();
         testObserver.awaitTerminalEvent();
@@ -266,6 +271,7 @@ public class UserServiceTest {
         testObserver.assertNoErrors();
 
         verify(userRepository, times(1)).delete("my-user");
+        verify(eventService, times(1)).create(any());
     }
 
     @Test