Repository: gravitee-io/graviteeio-access-management
Pull request #696 (2020-01-06 08:09:57)

fix(oauth2): introspection endpoint must not search for a user if tok…
…en was created via the client_credentials flow

fixes gravitee-io/issues#3068


Patch:
diff --git a/Makefile b/Makefile
index 879efcd792..ecec30c6d2 100644
--- a/Makefile
+++ b/Makefile
@@ -191,6 +191,7 @@ postman: ## Run postman non regression test (require newman npm module)
 	@newman run postman/collections/graviteeio-am-login-collection.json -e postman/environment/dev.json --ignore-redirects --insecure --bail
 	@newman run postman/collections/graviteeio-am-logout-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
+	@newman run postman/collections/graviteeio-am-api-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-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java
index 9cae27a647..003dd89ee8 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java
@@ -19,7 +19,6 @@ import io.gravitee.am.common.exception.oauth2.InvalidRequestException;
 import io.gravitee.am.common.oauth2.CodeChallengeMethod;
 import io.gravitee.am.common.oidc.Parameters;
 import io.gravitee.am.common.web.UriBuilder;
-import io.gravitee.am.gateway.handler.oauth2.exception.InvalidScopeException;
 import io.gravitee.am.gateway.handler.oauth2.exception.LoginRequiredException;
 import io.gravitee.am.gateway.handler.oauth2.service.pkce.PKCEUtils;
 import io.gravitee.am.gateway.handler.oidc.exception.ClaimsRequestSyntaxException;
@@ -62,9 +61,6 @@ public class AuthorizationRequestParseParametersHandler implements Handler<Routi
 
     @Override
     public void handle(RoutingContext context) {
-        // parse scope parameter
-        parseScopeParameter(context);
-
         // proceed prompt parameter
         parsePromptParameter(context);
 
@@ -80,14 +76,6 @@ public class AuthorizationRequestParseParametersHandler implements Handler<Routi
         context.next();
     }
 
-    private void parseScopeParameter(RoutingContext context) {
-        // Check scope parameter
-        String scopes = context.request().params().get(io.gravitee.am.common.oauth2.Parameters.SCOPE);
-        if (scopes != null && scopes.isEmpty()) {
-            throw new InvalidScopeException("Invalid parameter: scope must not be empty");
-        }
-    }
-
     private void parsePromptParameter(RoutingContext context) {
         String prompt = context.request().getParam(Parameters.PROMPT);
 
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/handler/token/TokenRequestParseHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/token/TokenRequestParseHandler.java
index cf08df8d8c..6df128f91e 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/token/TokenRequestParseHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/token/TokenRequestParseHandler.java
@@ -15,9 +15,8 @@
  */
 package io.gravitee.am.gateway.handler.oauth2.resources.handler.token;
 
-import io.gravitee.am.common.oauth2.Parameters;
 import io.gravitee.am.common.exception.oauth2.InvalidRequestException;
-import io.gravitee.am.gateway.handler.oauth2.exception.InvalidScopeException;
+import io.gravitee.am.common.oauth2.Parameters;
 import io.vertx.core.Handler;
 import io.vertx.reactivex.core.MultiMap;
 import io.vertx.reactivex.ext.web.RoutingContext;
@@ -43,9 +42,6 @@ public class TokenRequestParseHandler implements Handler<RoutingContext> {
         // proceed grant_type parameter
         parseGrantTypeParameter(context);
 
-        // proceed scope parameter
-        parseScopeParameter(context);
-
         context.next();
     }
 
@@ -69,12 +65,4 @@ public class TokenRequestParseHandler implements Handler<RoutingContext> {
             throw new InvalidRequestException("Missing parameter: grant_type");
         }
     }
-
-    private void parseScopeParameter(RoutingContext context) {
-        // Check scope parameter
-        String scopes = context.request().params().get(Parameters.SCOPE);
-        if (scopes != null && scopes.isEmpty()) {
-            throw new InvalidScopeException("Invalid parameter: scope must not be empty");
-        }
-    }
 }
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/request/AuthorizationRequestFactory.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/request/AuthorizationRequestFactory.java
index 64e7af7f95..5d76c171d2 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/request/AuthorizationRequestFactory.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/request/AuthorizationRequestFactory.java
@@ -70,7 +70,7 @@ public final class AuthorizationRequestFactory {
         authorizationRequest.setResponseType(request.params().get(Parameters.RESPONSE_TYPE));
         authorizationRequest.setRedirectUri(request.params().get(Parameters.REDIRECT_URI));
         String scope = request.params().get(Parameters.SCOPE);
-        authorizationRequest.setScopes(scope != null ? new HashSet<>(Arrays.asList(scope.split("\\s+"))) : null);
+        authorizationRequest.setScopes(scope != null && !scope.isEmpty() ? new HashSet<>(Arrays.asList(scope.split("\\s+"))) : null);
         authorizationRequest.setState(request.params().get(Parameters.STATE));
         authorizationRequest.setAdditionalParameters(extractAdditionalParameters(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/request/TokenRequestFactory.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/request/TokenRequestFactory.java
index 6626aaeedf..5fcced93d9 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/request/TokenRequestFactory.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/request/TokenRequestFactory.java
@@ -68,10 +68,8 @@ public final class TokenRequestFactory {
         // set OAuth 2.0 information
         tokenRequest.setClientId(request.params().get(Parameters.CLIENT_ID));
         tokenRequest.setGrantType(request.params().get(Parameters.GRANT_TYPE));
-        String scopes = request.params().get(Parameters.SCOPE);
-        if (scopes != null) {
-            tokenRequest.setScopes(new HashSet<>(Arrays.asList(scopes.split("\\s+"))));
-        }
+        String scope = request.params().get(Parameters.SCOPE);
+        tokenRequest.setScopes(scope != null && !scope.isEmpty() ? new HashSet<>(Arrays.asList(scope.split("\\s+"))) : null);
         tokenRequest.setAdditionalParameters(extractAdditionalParameters(request));
         return tokenRequest;
     }
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/introspection/impl/IntrospectionServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java
index 3309790d24..b474bdbf20 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java
@@ -43,7 +43,7 @@ public class IntrospectionServiceImpl implements IntrospectionService {
         return tokenService.introspect(introspectionRequest.getToken())
                 .flatMap(token -> {
                     AccessToken accessToken = (AccessToken) token;
-                    if (accessToken.getSubject() != null) {
+                    if (accessToken.getSubject() != null && !accessToken.getSubject().equals(accessToken.getClientId())) {
                         return userService
                                 .findById(accessToken.getSubject())
                                 .map(user -> convert(accessToken, user))
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/main/java/io/gravitee/am/management/handlers/admin/provider/security/DomainBasedAuthenticationProvider.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/main/java/io/gravitee/am/management/handlers/admin/provider/security/DomainBasedAuthenticationProvider.java
index 3e4ed07419..0a5dc9860d 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/main/java/io/gravitee/am/management/handlers/admin/provider/security/DomainBasedAuthenticationProvider.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/main/java/io/gravitee/am/management/handlers/admin/provider/security/DomainBasedAuthenticationProvider.java
@@ -57,12 +57,15 @@ public class DomainBasedAuthenticationProvider implements AuthenticationProvider
     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
         WebAuthenticationDetails webAuthenticationDetails = (WebAuthenticationDetails) authentication.getDetails();
         Map<String, String> details = new HashMap();
-        details.put(Claims.ip_address, webAuthenticationDetails.getRemoteAddress());
-        details.put(Claims.user_agent, webAuthenticationDetails.getUserAgent());
         details.put(Claims.domain, domain.getId());
+        if (webAuthenticationDetails != null) {
+            details.put(Claims.ip_address, webAuthenticationDetails.getRemoteAddress());
+            details.put(Claims.user_agent, webAuthenticationDetails.getUserAgent());
+        }
         Set<String> identities = domain.getIdentities();
         Iterator<String> iter = identities.iterator();
         io.gravitee.am.identityprovider.api.User user = null;
+        AuthenticationException lastException = null;
 
         // Create a end-user authentication for underlying providers associated to the domain
         io.gravitee.am.identityprovider.api.Authentication provAuthentication = new EndUserAuthentication(
@@ -75,19 +78,25 @@ public class DomainBasedAuthenticationProvider implements AuthenticationProvider
                     identityProviderManager.get(provider);
 
             if (authenticationProvider == null) {
-                throw new BadCredentialsException("Unable to load authentication provider " + provider + ", an error occurred during the initialization stage");
+                lastException = new BadCredentialsException("Unable to load authentication provider " + provider + ", an error occurred during the initialization stage");
+                continue;
             }
 
             try {
                 user = authenticationProvider.loadUserByUsername(provAuthentication).blockingGet();
                 // set user identity provider source
                 details.put(SOURCE, provider);
+                lastException = null;
             } catch (Exception ex) {
                 logger.info("Unable to authenticate user {} with provider {}", authentication.getName(), provider, ex);
-                throw new BadCredentialsException(ex.getMessage(), ex);
+                lastException = new BadCredentialsException(ex.getMessage(), ex);
             }
         }
 
+        if (lastException != null) {
+            throw lastException;
+        }
+
         if (user != null) {
             UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, provAuthentication.getCredentials(), AuthorityUtils.NO_AUTHORITIES);
             authenticationToken.setDetails(details);
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/UsersResource.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/UsersResource.java
index 0dafc9b934..a53031278b 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/UsersResource.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/UsersResource.java
@@ -1,157 +1,157 @@
-/**
- * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *         http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.gravitee.am.management.handlers.management.api.resources;
-
-import io.gravitee.am.management.service.UserService;
-import io.gravitee.am.model.User;
-import io.gravitee.am.model.common.Page;
-import io.gravitee.am.service.DomainService;
-import io.gravitee.am.service.IdentityProviderService;
-import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
-import io.gravitee.am.service.exception.DomainNotFoundException;
-import io.gravitee.am.service.exception.UserInvalidException;
-import io.gravitee.am.service.model.NewUser;
-import io.gravitee.common.http.MediaType;
-import io.reactivex.Maybe;
-import io.reactivex.Observable;
-import io.reactivex.Single;
-import io.swagger.annotations.*;
-import org.springframework.beans.factory.annotation.Autowired;
-
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
-import javax.ws.rs.*;
-import javax.ws.rs.container.AsyncResponse;
-import javax.ws.rs.container.ResourceContext;
-import javax.ws.rs.container.Suspended;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Response;
-import java.net.URI;
-import java.util.Comparator;
-
-/**
- * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
- * @author GraviteeSource Team
- */
-@Api(tags = {"user"})
-public class UsersResource extends AbstractResource {
-
-    private static final int MAX_USERS_SIZE_PER_PAGE = 30;
-    private static final String MAX_USERS_SIZE_PER_PAGE_STRING = "30";
-
-    @Context
-    private ResourceContext resourceContext;
-
-    @Autowired
-    private UserService userService;
-
-    @Autowired
-    private DomainService domainService;
-
-    @Autowired
-    private IdentityProviderService identityProviderService;
-
-    @Autowired
-    private PasswordValidator passwordValidator;
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @ApiOperation(value = "List users for a security domain")
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "List users for a security domain", response = User.class, responseContainer = "Set"),
-            @ApiResponse(code = 500, message = "Internal server error")})
-    public void list(@PathParam("domain") String domain,
-                                @QueryParam("q") String query,
-                                @QueryParam("page") @DefaultValue("0") int page,
-                                @QueryParam("size") @DefaultValue(MAX_USERS_SIZE_PER_PAGE_STRING) int size,
-                                @Suspended final AsyncResponse response) {
-        domainService.findById(domain)
-                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
-                .flatMapSingle(irrelevant -> {
-                    if (query != null) {
-                        return userService.search(domain, query, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
-                    } else {
-                        return userService.findByDomain(domain, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
-                    }
-                })
-                .flatMap(pagedUsers ->
-                        Observable.fromIterable(pagedUsers.getData())
-                            .flatMapSingle(user -> {
-                                if (user.getSource() != null) {
-                                    return identityProviderService.findById(user.getSource())
-                                            .map(idP -> {
-                                                user.setSource(idP.getName());
-                                                return user;
-                                            })
-                                            .defaultIfEmpty(user)
-                                            .toSingle();
-                                }
-                                return Single.just(user);
-                            })
-                            .toSortedList(Comparator.comparing(User::getUsername))
-                            .map(users -> new Page(users, pagedUsers.getCurrentPage(), pagedUsers.getTotalCount()))
-                )
-                .map(users -> Response.ok(users).build())
-                .subscribe(
-                        result -> response.resume(result),
-                        error -> response.resume(error));
-    }
-
-    @POST
-    @Produces(MediaType.APPLICATION_JSON)
-    @Consumes(MediaType.APPLICATION_JSON)
-    @ApiOperation(value = "Create a user")
-    @ApiResponses({
-            @ApiResponse(code = 201, message = "User successfully created"),
-            @ApiResponse(code = 500, message = "Internal server error")})
-    public void create(
-            @PathParam("domain") String domain,
-            @ApiParam(name = "user", required = true)
-            @Valid @NotNull final NewUser newUser,
-            @Suspended final AsyncResponse response) {
-        final io.gravitee.am.identityprovider.api.User authenticatedUser = getAuthenticatedUser();
-
-        // user must have a password in no pre registration mode
-        if (!newUser.isPreRegistration() && newUser.getPassword() == null) {
-            response.resume(new UserInvalidException(("Field [password] is required")));
-            return;
-        }
-
-        // check password policy
-        if (newUser.getPassword() != null) {
-            if (!passwordValidator.validate(newUser.getPassword())) {
-                response.resume(new UserInvalidException(("Field [password] is invalid")));
-                return;
-            }
-        }
-
-        domainService.findById(domain)
-                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
-                .flatMapSingle(userProvider -> userService.create(domain, newUser, authenticatedUser))
-                .map(user -> Response
-                        .created(URI.create("/domains/" + domain + "/users/" + user.getId()))
-                        .entity(user)
-                        .build())
-                .subscribe(
-                        result -> response.resume(result),
-                        error -> response.resume(error));
-    }
-
-    @Path("{user}")
-    public UserResource getUserResource() {
-        return resourceContext.getResource(UserResource.class);
-    }
-}
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.management.handlers.management.api.resources;
+
+import io.gravitee.am.management.service.UserService;
+import io.gravitee.am.model.User;
+import io.gravitee.am.model.common.Page;
+import io.gravitee.am.service.DomainService;
+import io.gravitee.am.service.IdentityProviderService;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
+import io.gravitee.am.service.exception.DomainNotFoundException;
+import io.gravitee.am.service.exception.UserInvalidException;
+import io.gravitee.am.service.model.NewUser;
+import io.gravitee.common.http.MediaType;
+import io.reactivex.Maybe;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+import io.swagger.annotations.*;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.*;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.ResourceContext;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+import java.util.Comparator;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@Api(tags = {"user"})
+public class UsersResource extends AbstractResource {
+
+    private static final int MAX_USERS_SIZE_PER_PAGE = 30;
+    private static final String MAX_USERS_SIZE_PER_PAGE_STRING = "30";
+
+    @Context
+    private ResourceContext resourceContext;
+
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private DomainService domainService;
+
+    @Autowired
+    private IdentityProviderService identityProviderService;
+
+    @Autowired
+    private PasswordValidator passwordValidator;
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(value = "List users for a security domain")
+    @ApiResponses({
+            @ApiResponse(code = 200, message = "List users for a security domain", response = User.class, responseContainer = "Set"),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void list(@PathParam("domain") String domain,
+                                @QueryParam("q") String query,
+                                @QueryParam("page") @DefaultValue("0") int page,
+                                @QueryParam("size") @DefaultValue(MAX_USERS_SIZE_PER_PAGE_STRING) int size,
+                                @Suspended final AsyncResponse response) {
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMapSingle(irrelevant -> {
+                    if (query != null) {
+                        return userService.search(domain, query, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
+                    } else {
+                        return userService.findByDomain(domain, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
+                    }
+                })
+                .flatMap(pagedUsers ->
+                        Observable.fromIterable(pagedUsers.getData())
+                            .flatMapSingle(user -> {
+                                if (user.getSource() != null) {
+                                    return identityProviderService.findById(user.getSource())
+                                            .map(idP -> {
+                                                user.setSource(idP.getName());
+                                                return user;
+                                            })
+                                            .defaultIfEmpty(user)
+                                            .toSingle();
+                                }
+                                return Single.just(user);
+                            })
+                            .toSortedList(Comparator.comparing(User::getUsername))
+                            .map(users -> new Page(users, pagedUsers.getCurrentPage(), pagedUsers.getTotalCount()))
+                )
+                .map(users -> Response.ok(users).build())
+                .subscribe(
+                        result -> response.resume(result),
+                        error -> response.resume(error));
+    }
+
+    @POST
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    @ApiOperation(value = "Create a user")
+    @ApiResponses({
+            @ApiResponse(code = 201, message = "User successfully created"),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void create(
+            @PathParam("domain") String domain,
+            @ApiParam(name = "user", required = true)
+            @Valid @NotNull final NewUser newUser,
+            @Suspended final AsyncResponse response) {
+        final io.gravitee.am.identityprovider.api.User authenticatedUser = getAuthenticatedUser();
+
+        // user must have a password in no pre registration mode
+        if (!newUser.isPreRegistration() && newUser.getPassword() == null) {
+            response.resume(new UserInvalidException(("Field [password] is required")));
+            return;
+        }
+
+        // check password policy
+        if (newUser.getPassword() != null) {
+            if (!passwordValidator.validate(newUser.getPassword())) {
+                response.resume(new UserInvalidException(("Field [password] is invalid")));
+                return;
+            }
+        }
+
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMapSingle(userProvider -> userService.create(domain, newUser, authenticatedUser))
+                .map(user -> Response
+                        .created(URI.create("/domains/" + domain + "/users/" + user.getId()))
+                        .entity(user)
+                        .build())
+                .subscribe(
+                        result -> response.resume(result),
+                        error -> response.resume(error));
+    }
+
+    @Path("{user}")
+    public UserResource getUserResource() {
+        return resourceContext.getResource(UserResource.class);
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/UserRepositoryProxy.java b/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/UserRepositoryProxy.java
index 4ac215228c..49bfedea63 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/UserRepositoryProxy.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/UserRepositoryProxy.java
@@ -44,8 +44,8 @@ public class UserRepositoryProxy extends AbstractProxy<UserRepository> implement
     }
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int limit) {
-        return target.search(domain, query, limit);
+    public Single<Page<User>> search(String domain, String query, int page, int size) {
+        return target.search(domain, query, page, size);
     }
 
     @Override
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 2444121ef0..de4fa868e5 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
@@ -31,7 +31,7 @@ import java.util.List;
  */
 public interface UserService {
 
-    Single<Page<User>> search(String domain, String query, int limit);
+    Single<Page<User>> search(String domain, String query, int page, int size);
 
     Single<Page<User>> findByDomain(String domain, int page, int size);
 
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 12eb658a23..34e9085e44 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
@@ -100,8 +100,8 @@ public class UserServiceImpl implements UserService {
     private RoleService roleService;
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int limit) {
-        return userService.search(domain, query, limit);
+    public Single<Page<User>> search(String domain, String query, int page, int size) {
+        return userService.search(domain, query, page, size);
     }
 
     @Override
diff --git a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/UserRepository.java b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/UserRepository.java
index 1bf36ac05e..32fc884a35 100644
--- a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/UserRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/UserRepository.java
@@ -34,7 +34,7 @@ public interface UserRepository extends CrudRepository<User, String> {
 
     Single<Page<User>> findByDomain(String domain, int page, int size);
 
-    Single<Page<User>> search(String domain, String query, int limit);
+    Single<Page<User>> search(String domain, String query, int page, int size);
 
     Single<List<User>> findByDomainAndEmail(String domain, String email, boolean strict);
 
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoUserRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoUserRepository.java
index f0d4afcd17..1196f847c0 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoUserRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoUserRepository.java
@@ -84,7 +84,7 @@ public class MongoUserRepository extends AbstractManagementMongoRepository imple
     }
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int limit) {
+    public Single<Page<User>> search(String domain, String query, int page, int size) {
         // currently search on username field
         Bson searchQuery = new BasicDBObject(FIELD_USERNAME, query);
         // if query contains wildcard, use the regex query
@@ -99,7 +99,7 @@ public class MongoUserRepository extends AbstractManagementMongoRepository imple
                 searchQuery);
 
         Single<Long> countOperation = Observable.fromPublisher(usersCollection.countDocuments(mongoQuery)).first(0l);
-        Single<Set<User>> usersOperation = Observable.fromPublisher(usersCollection.find(mongoQuery).limit(limit)).map(this::convert).collect(LinkedHashSet::new, Set::add);
+        Single<Set<User>> usersOperation = Observable.fromPublisher(usersCollection.find(mongoQuery).skip(size * page).limit(size)).map(this::convert).collect(LinkedHashSet::new, Set::add);
         return Single.zip(countOperation, usersOperation, (count, users) -> new Page<>(users, 0, count));
     }
 
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java
index 7aa2019ed4..dab97741ed 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java
@@ -37,7 +37,7 @@ public interface UserService {
 
     Single<Page<User>> findByDomain(String domain, int page, int size);
 
-    Single<Page<User>> search(String domain, String query, int limit);
+    Single<Page<User>> search(String domain, String query, int page, int size);
 
     Single<List<User>> findByIdIn(List<String> ids);
 
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 22403134d9..db692522c0 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
@@ -80,9 +80,9 @@ public class UserServiceImpl implements UserService {
     }
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int limit) {
+    public Single<Page<User>> search(String domain, String query, int page, int size) {
         LOGGER.debug("Search users for domain {} with query {}", domain, query);
-        return userRepository.search(domain, query, limit)
+        return userRepository.search(domain, query, page, size)
                 .onErrorResumeNext(ex -> {
                     LOGGER.error("An error occurs while trying to search users for domain {} and query {}", domain, query, ex);
                     return Single.error(new TechnicalManagementException(String.format("An error occurs while trying to find users for domain %s and query %s", domain, query), ex));
diff --git a/postman/collections/graviteeio-am-api-management-collection.json b/postman/collections/graviteeio-am-api-management-collection.json
new file mode 100644
index 0000000000..2e41d7e70e
--- /dev/null
+++ b/postman/collections/graviteeio-am-api-management-collection.json
@@ -0,0 +1,361 @@
+{
+	"info": {
+		"_postman_id": "c55b4035-63d4-4790-9ee3-ba5306246751",
+		"name": "Gravitee.io - AM - API Management",
+		"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": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Login",
+			"item": [
+				{
+					"name": "Get admin configuration",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "86a05f9d-bc53-4105-b7b5-7158e9ac9339",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"var jsonData = pm.response.json();",
+									"pm.environment.set('currentIdp', jsonData.identities[0]);"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}",
+								"type": "text"
+							}
+						],
+						"url": {
+							"raw": "{{management_url}}/management/domains/admin",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"admin"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create new identity provider",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "d156cb2e-9446-4a09-ae08-fb1717c61f7a",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});",
+									"",
+									"var jsonData = pm.response.json();",
+									"pm.environment.set('newIdp', jsonData.id);"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"value": "application/json",
+								"type": "text"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}",
+								"type": "text"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\"external\":false,\n\t\"type\":\"inline-am-idp\",\n\t\"configuration\":\"{\\\"users\\\":[{\\\"firstname\\\":\\\"test\\\",\\\"lastname\\\":\\\"test\\\",\\\"username\\\":\\\"test\\\",\\\"email\\\":\\\"test@test.com\\\",\\\"password\\\":\\\"test\\\"}]}\",\n\t\"name\":\"Inline Users 2\"\n}",
+							"options": {
+								"raw": {
+									"language": "json"
+								}
+							}
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/admin/identities",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"admin",
+								"identities"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update admin configuration",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"value": "application/json",
+								"type": "text"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}",
+								"type": "text"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\"identities\":[\"{{currentIdp}}\",\"{{newIdp}}\"]}",
+							"options": {
+								"raw": {
+									"language": "json"
+								}
+							}
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/admin",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"admin"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"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);",
+									"});"
+								],
+								"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": "test",
+									"type": "text"
+								},
+								{
+									"key": "password",
+									"value": "test",
+									"type": "text"
+								}
+							]
+						},
+						"url": {
+							"raw": "{{management_url}}/admin/token",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"admin",
+								"token"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Reset admin configuration",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\"identities\":[\"{{currentIdp}}\"]}",
+							"options": {
+								"raw": {
+									"language": "json"
+								}
+							}
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/admin",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"admin"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete new identity provider",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "d156cb2e-9446-4a09-ae08-fb1717c61f7a",
+								"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}}"
+							}
+						],
+						"url": {
+							"raw": "{{management_url}}/management/domains/admin/identities/{{newIdp}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"admin",
+								"identities",
+								"{{newIdp}}"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		}
+	],
+	"protocolProfileBehavior": {}
+}
\ No newline at end of file
diff --git a/postman/collections/graviteeio-am-oauth2-collection.json b/postman/collections/graviteeio-am-oauth2-collection.json
index fc70e02f63..6cc7251ac6 100644
--- a/postman/collections/graviteeio-am-oauth2-collection.json
+++ b/postman/collections/graviteeio-am-oauth2-collection.json
@@ -1,6 +1,6 @@
 {
 	"info": {
-		"_postman_id": "363baad7-40ce-452c-a5e1-71e611a8b857",
+		"_postman_id": "3623be71-518f-441d-99d6-9c1564f165ca",
 		"name": "Gravitee.io - AM - Oauth2",
 		"description": "Test Oauth2 (RFC 6749) specifications",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
@@ -1043,14 +1043,8 @@
 									"script": {
 										"id": "1ae3c79d-d2e7-49ab-bd20-70629648df50",
 										"exec": [
-											"pm.test(\"Status code is 400\", function () {",
-											"    pm.response.to.have.status(400);",
-											"});",
-											"",
-											"pm.test(\"Invalid scope\", function () {",
-											"    pm.response.to.be.header('Content-Type', 'application/json');",
-											"    var body = pm.response.json();",
-											"    pm.expect(body.error).to.eql('invalid_scope');",
+											"pm.test(\"Status code is 200\", function () {",
+											"    pm.response.to.have.status(200);",
 											"});"
 										],
 										"type": "text/javascript"
@@ -1859,14 +1853,8 @@
 									"script": {
 										"id": "e9d398f2-b014-4b0a-9c2d-e116fc99298d",
 										"exec": [
-											"pm.test(\"Status code is 400\", function () {",
-											"    pm.response.to.have.status(400);",
-											"});",
-											"",
-											"pm.test(\"Invalid grant\", function () {",
-											"    pm.response.to.be.header('Content-Type', 'application/json');",
-											"    var body = pm.response.json();",
-											"    pm.expect(body.error).to.eql('invalid_scope');",
+											"pm.test(\"Status code is 200\", function () {",
+											"    pm.response.to.have.status(200);",
 											"});"
 										],
 										"type": "text/javascript"
@@ -13107,4 +13095,4 @@
 		}
 	],
 	"protocolProfileBehavior": {}
-}
+}
\ No newline at end of file


Test patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java
index dd0700924d..00824236b0 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java
@@ -157,13 +157,28 @@ public class AuthorizationEndpointTest extends RxWebTestBase {
     }
 
     @Test
-    public void shouldNotInvokeAuthorizationEndpoint_emptyScope() throws Exception {
+    public void shouldInvokeAuthorizationEndpoint_emptyScope() throws Exception {
         final Client client = new Client();
         client.setId("client-id");
         client.setClientId("client-id");
         client.setRedirectUris(Collections.singletonList("http://localhost:9999/callback"));
 
+        AuthorizationRequest authorizationRequest = new AuthorizationRequest();
+        authorizationRequest.setApproved(true);
+        authorizationRequest.setResponseType(ResponseType.CODE);
+        authorizationRequest.setRedirectUri("http://localhost:9999/callback");
+
+        AuthorizationResponse authorizationResponse = new AuthorizationCodeResponse();
+        authorizationResponse.setRedirectUri(authorizationRequest.getRedirectUri());
+        ((AuthorizationCodeResponse) authorizationResponse).setCode("test-code");
+
+        router.route().order(-1).handler(routingContext -> {
+            routingContext.setUser(new User(new io.gravitee.am.gateway.handler.common.vertx.web.auth.user.User(new io.gravitee.am.model.User())));
+            routingContext.next();
+        });
+
         when(clientSyncService.findByClientId("client-id")).thenReturn(Maybe.just(client));
+        when(flow.run(any(), any(), any())).thenReturn(Single.just(authorizationResponse));
 
         router.route().order(-1).handler(routingContext -> {
             routingContext.put(CLIENT_CONTEXT_KEY, client);
@@ -176,7 +191,7 @@ public class AuthorizationEndpointTest extends RxWebTestBase {
                 resp -> {
                     String location = resp.headers().get("location");
                     assertNotNull(location);
-                    assertEquals("http://localhost:9999/callback?error=invalid_scope&error_description=Invalid+parameter%253A+scope+must+not+be+empty", location);
+                    assertEquals("http://localhost:9999/callback?code=test-code", location);
                 },
                 HttpStatusCode.FOUND_302, "Found", null);
     }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/TokenEndpointTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/TokenEndpointTest.java
index f1666e330c..e7b7774ea2 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/TokenEndpointTest.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/TokenEndpointTest.java
@@ -73,10 +73,10 @@ public class TokenEndpointTest extends RxWebTestBase {
     }
 
     @Test
-    public void shouldNotInvokeTokenEndpoint_emptyScope() throws Exception {
+    public void shouldInvokeTokenEndpoint_emptyScope() throws Exception {
         testRequest(
                 HttpMethod.POST, "/oauth/token?grant_type=client_credentials&scope=",
-                HttpStatusCode.BAD_REQUEST_400, "Bad Request");
+                HttpStatusCode.UNAUTHORIZED_401, "Unauthorized");
     }
 
     @Test
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/introspection/IntrospectionServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/IntrospectionServiceTest.java
new file mode 100644
index 0000000000..d441d7995a
--- /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/introspection/IntrospectionServiceTest.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.gateway.handler.oauth2.service.introspection;
+
+import io.gravitee.am.gateway.handler.oauth2.service.introspection.impl.IntrospectionServiceImpl;
+import io.gravitee.am.gateway.handler.oauth2.service.token.TokenService;
+import io.gravitee.am.gateway.handler.oauth2.service.token.impl.AccessToken;
+import io.gravitee.am.model.User;
+import io.gravitee.am.service.UserService;
+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.junit.MockitoJUnitRunner;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class IntrospectionServiceTest {
+
+    @InjectMocks
+    private IntrospectionService introspectionService = new IntrospectionServiceImpl();
+
+    @Mock
+    private TokenService tokenService;
+
+    @Mock
+    private UserService userService;
+
+    @Test
+    public void shouldSearchForAUser() {
+        final String token = "token";
+        AccessToken accessToken = new AccessToken(token);
+        accessToken.setSubject("user");
+        accessToken.setClientId("client-id");
+        when(tokenService.introspect("token")).thenReturn(Single.just(accessToken));
+        when(userService.findById("user")).thenReturn(Maybe.just(new User()));
+
+        IntrospectionRequest introspectionRequest = new IntrospectionRequest(token);
+        TestObserver<IntrospectionResponse> testObserver = introspectionService.introspect(introspectionRequest).test();
+
+        testObserver.awaitTerminalEvent();
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        verify(userService, times(1)).findById("user");
+    }
+
+    @Test
+    public void shouldNotSearchForAUser_clientCredentials() {
+        final String token = "token";
+        AccessToken accessToken = new AccessToken(token);
+        accessToken.setSubject("client-id");
+        accessToken.setClientId("client-id");
+        when(tokenService.introspect("token")).thenReturn(Single.just(accessToken));
+
+        IntrospectionRequest introspectionRequest = new IntrospectionRequest(token);
+        TestObserver<IntrospectionResponse> testObserver = introspectionService.introspect(introspectionRequest).test();
+
+        testObserver.awaitTerminalEvent();
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        verify(userService, never()).findById(anyString());
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/test/java/io/gravitee/am/management/handlers/admin/provider/security/DomainBasedAuthenticationProviderTest.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/test/java/io/gravitee/am/management/handlers/admin/provider/security/DomainBasedAuthenticationProviderTest.java
new file mode 100644
index 0000000000..fd32a58e8e
--- /dev/null
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/test/java/io/gravitee/am/management/handlers/admin/provider/security/DomainBasedAuthenticationProviderTest.java
@@ -0,0 +1,122 @@
+/**
+ * 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.admin.provider.security;
+
+import io.gravitee.am.common.exception.authentication.BadCredentialsException;
+import io.gravitee.am.identityprovider.api.AuthenticationProvider;
+import io.gravitee.am.identityprovider.api.DefaultUser;
+import io.gravitee.am.management.handlers.admin.security.IdentityProviderManager;
+import io.gravitee.am.model.Domain;
+import io.reactivex.Maybe;
+import org.junit.Assert;
+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 org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DomainBasedAuthenticationProviderTest {
+
+    @InjectMocks
+    private DomainBasedAuthenticationProvider domainBasedAuthenticationProvider =  new DomainBasedAuthenticationProvider();
+
+    @Mock
+    private Domain domain;
+
+    @Mock
+    private IdentityProviderManager identityProviderManager;
+
+    @Before
+    public void init() {
+        when(domain.getIdentities()).thenReturn(new LinkedHashSet<>(Arrays.asList("idp1", "idp2")));
+    }
+
+    @Test
+    public void shouldAuthenticate_firstAuthProvider() {
+        AuthenticationProvider authenticationProvider = mock(AuthenticationProvider.class);
+        when(authenticationProvider.loadUserByUsername(any(io.gravitee.am.identityprovider.api.Authentication.class))).thenReturn(Maybe.just(new DefaultUser("username")));
+        when(identityProviderManager.get("idp1")).thenReturn(authenticationProvider);
+
+        Authentication authentication = domainBasedAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken("username", "password"));
+
+        Assert.assertNotNull(authentication);
+        verify(identityProviderManager, times(1)).get("idp1");
+        verify(identityProviderManager, never()).get("idp2");
+    }
+
+    @Test
+    public void shouldAuthenticate_secondAuthProvider() {
+        AuthenticationProvider authenticationProvider = mock(AuthenticationProvider.class);
+        when(authenticationProvider.loadUserByUsername(any(io.gravitee.am.identityprovider.api.Authentication.class))).thenReturn(Maybe.error(new BadCredentialsException()));
+        AuthenticationProvider authenticationProvider2 = mock(AuthenticationProvider.class);
+        when(authenticationProvider2.loadUserByUsername(any(io.gravitee.am.identityprovider.api.Authentication.class))).thenReturn(Maybe.just(new DefaultUser("username")));
+        when(identityProviderManager.get("idp1")).thenReturn(authenticationProvider);
+        when(identityProviderManager.get("idp2")).thenReturn(authenticationProvider2);
+
+        Authentication authentication = domainBasedAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken("username", "password"));
+
+        Assert.assertNotNull(authentication);
+        verify(identityProviderManager, times(1)).get("idp1");
+        verify(identityProviderManager, times(1)).get("idp2");
+
+    }
+
+    @Test
+    public void shouldAuthenticate_secondAuthProvider_firstNull() {
+        AuthenticationProvider authenticationProvider2 = mock(AuthenticationProvider.class);
+        when(authenticationProvider2.loadUserByUsername(any(io.gravitee.am.identityprovider.api.Authentication.class))).thenReturn(Maybe.just(new DefaultUser("username")));
+        when(identityProviderManager.get("idp1")).thenReturn(null);
+        when(identityProviderManager.get("idp2")).thenReturn(authenticationProvider2);
+
+        Authentication authentication = domainBasedAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken("username", "password"));
+
+        Assert.assertNotNull(authentication);
+        verify(identityProviderManager, times(1)).get("idp1");
+        verify(identityProviderManager, times(1)).get("idp2");
+
+    }
+
+    @Test(expected = org.springframework.security.authentication.BadCredentialsException.class)
+    public void shouldNotAuthenticate_wrongCredentials() {
+        AuthenticationProvider authenticationProvider = mock(AuthenticationProvider.class);
+        when(authenticationProvider.loadUserByUsername(any(io.gravitee.am.identityprovider.api.Authentication.class))).thenReturn(Maybe.error(new BadCredentialsException()));
+        AuthenticationProvider authenticationProvider2 = mock(AuthenticationProvider.class);
+        when(authenticationProvider2.loadUserByUsername(any(io.gravitee.am.identityprovider.api.Authentication.class))).thenReturn(Maybe.error(new BadCredentialsException()));
+        when(identityProviderManager.get("idp1")).thenReturn(authenticationProvider);
+        when(identityProviderManager.get("idp2")).thenReturn(authenticationProvider2);
+
+        Authentication authentication = domainBasedAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken("username", "password"));
+
+        Assert.assertNull(authentication);
+        verify(identityProviderManager, times(1)).get("idp1");
+        verify(identityProviderManager, times(1)).get("idp2");
+
+    }
+}
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoUserRepositoryTest.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoUserRepositoryTest.java
index eb2b6695d3..2a5a5ffa38 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoUserRepositoryTest.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoUserRepositoryTest.java
@@ -24,6 +24,7 @@ import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.Set;
 
 /**
@@ -148,7 +149,7 @@ public class MongoUserRepositoryTest extends AbstractManagementRepositoryTest {
         userRepository.create(user2).blockingGet();
 
         // fetch user
-        TestObserver<Page<User>> testObserver = userRepository.search(domain, "testUsername", 10).test();
+        TestObserver<Page<User>> testObserver = userRepository.search(domain, "testUsername", 0, 10).test();
         testObserver.awaitTerminalEvent();
 
         testObserver.assertComplete();
@@ -173,12 +174,53 @@ public class MongoUserRepositoryTest extends AbstractManagementRepositoryTest {
         userRepository.create(user2).blockingGet();
 
         // fetch user
-        TestObserver<Page<User>> testObserver = userRepository.search(domain, "testUsername*", 10).test();
+        TestObserver<Page<User>> testObserver = userRepository.search(domain, "testUsername*", 0, 10).test();
         testObserver.awaitTerminalEvent();
 
         testObserver.assertComplete();
         testObserver.assertNoErrors();
         testObserver.assertValue(users -> users.getData().size() == 2);
     }
+    
+    @Test
+    public void testSearch_paged() {
+        final String domain = "domain";
+        // create user
+        User user1 = new User();
+        user1.setDomain(domain);
+        user1.setUsername("testUsername1");
+        userRepository.create(user1).blockingGet();
+
+        User user2 = new User();
+        user2.setDomain(domain);
+        user2.setUsername("testUsername2");
+        userRepository.create(user2).blockingGet();
+        
+        User user3 = new User();
+        user3.setDomain(domain);
+        user3.setUsername("testUsername3");
+        userRepository.create(user3).blockingGet();
+
+        // fetch user (page 0)
+        TestObserver<Page<User>> testObserverP0 = userRepository.search(domain, "testUsername*", 0, 2).test();
+        testObserverP0.awaitTerminalEvent();
+        
+        testObserverP0.assertComplete();
+        testObserverP0.assertNoErrors();
+        testObserverP0.assertValue(users -> users.getData().size() == 2);
+        testObserverP0.assertValue(users -> {
+        	Iterator<User> it = users.getData().iterator();
+        	return it.next().getUsername().equals(user1.getUsername()) && it.next().getUsername().equals(user2.getUsername());
+        });
+        
+        // fetch user (page 1)
+        TestObserver<Page<User>> testObserverP1 = userRepository.search(domain, "testUsername*", 1, 2).test();
+        testObserverP1.awaitTerminalEvent();
+        
+        testObserverP1.assertComplete();
+        testObserverP1.assertNoErrors();
+        testObserverP1.assertValue(users -> users.getData().size() == 1);
+        testObserverP1.assertValue(users -> users.getData().iterator().next().getUsername().equals(user3.getUsername()));
+    }
 
 }