Repository: gravitee-io/graviteeio-access-management
Pull request #925 (2020-09-28 16:05:44)

feat(users): add advanced search feature based on SCIM 2.0 syntax
closes gravitee-io/issues#3227 and closes gravitee-io/issues#3937


Patch:
diff --git a/docs/scim-api-descriptor.yml b/docs/scim-api-descriptor.yml
index 4060c24c33..2e7f587b04 100644
--- a/docs/scim-api-descriptor.yml
+++ b/docs/scim-api-descriptor.yml
@@ -53,6 +53,11 @@ paths:
       produces:
       - application/json
       parameters:
+      - in: query
+        name: filter
+        type: string
+        description: Filter expression used to restrict results to one or more specific resource types.
+        example: userName eq "john"
       - in: query
         name: startIndex
         type: integer
diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/CommonAttribute.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/CommonAttribute.java
new file mode 100644
index 0000000000..78f8e8a493
--- /dev/null
+++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/CommonAttribute.java
@@ -0,0 +1,38 @@
+/**
+ * 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.common.scim;
+
+/**
+ * See <a href="https://tools.ietf.org/html/rfc7643#section-3.1">3.1.  Common Attributes</a>
+ *
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface CommonAttribute {
+
+    /**
+     * A unique identifier for a SCIM resource as defined by the service provider.
+     */
+    String ID = "id";
+    /**
+     * A String that is an identifier for the resource as defined by the provisioning client.
+     */
+    String EXTERNAL_ID= "externalId";
+    /**
+     * A complex attribute containing resource metadata.
+     */
+    String META = "meta";
+}
diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/Schema.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/Schema.java
new file mode 100644
index 0000000000..b4489b32ab
--- /dev/null
+++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/Schema.java
@@ -0,0 +1,27 @@
+/**
+ * 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.common.scim;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface Schema {
+
+    String SCHEMA_URI_CORE = "urn:ietf:params:scim:schemas:core:2.0";
+    String SCHEMA_URI_USER = "urn:ietf:params:scim:schemas:core:2.0:User";
+    String SCHEMA_URI_GROUP = "urn:ietf:params:scim:schemas:core:2.0:Group";
+}
diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/filter/AttributePath.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/filter/AttributePath.java
new file mode 100644
index 0000000000..b6176f38da
--- /dev/null
+++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/filter/AttributePath.java
@@ -0,0 +1,152 @@
+/**
+ * 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.common.scim.filter;
+
+import io.gravitee.am.common.scim.CommonAttribute;
+import io.gravitee.am.common.scim.Schema;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ *  attrPath  = [URI ":"] ATTRNAME *1subAttr
+ *                    ; SCIM attribute name
+ *                    ; URI is SCIM "schema" URI
+ *
+ * See <a href="https://tools.ietf.org/html/rfc7644#section-3.4.2.2">3.4.2.2. Filtering</a>
+ *
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class AttributePath {
+    /**
+     * A regular expression to match the components of an attribute path.
+     * Given the path "urn:scim:schemas:core:user:1.0:name.familyName" then
+     * group 2 will match "urn:scim:schemas:core:user:1.0", group 3 matches
+     * "name" and group 5 matches "familyName".
+     */
+    private static final Pattern pattern = Pattern.compile("^((.+):)?([^.]+)(\\.(.+))?$");
+
+    /**
+     * The URI of the attribute schema.
+     */
+    private final String attributeSchema;
+
+    /**
+     * The name of the attribute.
+     */
+    private final String attributeName;
+
+    /**
+     * The name of the sub-attribute, or {@code null} if absent.
+     */
+    private final String subAttributeName;
+
+    /**
+     * Create a new attribute path.
+     *
+     * @param attributeSchema   The URI of the attribute schema.
+     * @param attributeName     The name of the attribute.
+     * @param subAttributeName  The name of the sub-attribute, or {@code null} if
+     *                          absent.
+     */
+    public AttributePath(final String attributeSchema,
+                         final String attributeName,
+                         final String subAttributeName) {
+        this.attributeSchema  = attributeSchema;
+        this.attributeName    = attributeName;
+        this.subAttributeName = subAttributeName;
+    }
+
+    /**
+     * Parse an attribute path.
+     *
+     * @param path  The attribute path.
+     * @param defaultSchema The default schema to assume for attributes that do
+     *                      not have the schema part of the urn specified. The
+     *                      'id', 'externalId', and 'meta' attributes will always
+     *                      assume the SCIM Core schema.
+     *
+     * @return The parsed attribute path.
+     */
+    public static AttributePath parse(final String path,
+                                      final String defaultSchema) {
+        final Matcher matcher = pattern.matcher(path);
+
+        if (!matcher.matches() || matcher.groupCount() != 5) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "'%s' does not match '[schema:]attr[.sub-attr]' format", path));
+        }
+
+        final String attributeSchema = matcher.group(2);
+        final String attributeName = matcher.group(3);
+        final String subAttributeName = matcher.group(5);
+
+        if (attributeSchema != null) {
+            return new AttributePath(attributeSchema, attributeName,
+                    subAttributeName);
+        } else {
+            if (attributeName.equalsIgnoreCase(
+                    CommonAttribute.ID) ||
+                    attributeName.equalsIgnoreCase(
+                            CommonAttribute.EXTERNAL_ID) ||
+                    attributeName.equalsIgnoreCase(
+                            CommonAttribute.META)) {
+                return new AttributePath(Schema.SCHEMA_URI_CORE, attributeName,
+                        subAttributeName);
+            } else {
+                return new AttributePath(defaultSchema, attributeName,
+                        subAttributeName);
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        toString(builder);
+        return builder.toString();
+    }
+
+    private void toString(final StringBuilder builder) {
+        if (!attributeSchema.equalsIgnoreCase(Schema.SCHEMA_URI_CORE)) {
+            builder.append(attributeSchema);
+            builder.append(':');
+        }
+
+        builder.append(attributeName);
+        if (subAttributeName != null) {
+            builder.append('.');
+            builder.append(subAttributeName);
+        }
+    }
+
+    public String getAttributeSchema()
+    {
+        return attributeSchema;
+    }
+
+    public String getAttributeName()
+    {
+        return attributeName;
+    }
+
+    public String getSubAttributeName()
+    {
+        return subAttributeName;
+    }
+}
diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/filter/Filter.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/filter/Filter.java
new file mode 100644
index 0000000000..207c8757b6
--- /dev/null
+++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/filter/Filter.java
@@ -0,0 +1,135 @@
+/**
+ * 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.common.scim.filter;
+
+import java.util.List;
+
+/**
+ * SCIM filters MUST conform to the following ABNF [RFC5234] rules as
+ *    specified below:
+ *
+ *      FILTER    = attrExp / logExp / valuePath / *1"not" "(" FILTER ")"
+ *
+ *      valuePath = attrPath "[" valFilter "]"
+ *                  ; FILTER uses sub-attributes of a parent attrPath
+ *
+ *      valFilter = attrExp / logExp / *1"not" "(" valFilter ")"
+ *
+ *      attrExp   = (attrPath SP "pr") /
+ *                  (attrPath SP compareOp SP compValue)
+ *
+ *      logExp    = FILTER SP ("and" / "or") SP FILTER
+ *
+ *      compValue = false / null / true / number / string
+ *                  ; rules from JSON (RFC 7159)
+ *
+ *      compareOp = "eq" / "ne" / "co" /
+ *                         "sw" / "ew" /
+ *                         "gt" / "lt" /
+ *                         "ge" / "le"
+ *
+ *      attrPath  = [URI ":"] ATTRNAME *1subAttr
+ *                  ; SCIM attribute name
+ *                  ; URI is SCIM "schema" URI
+ *
+ *      ATTRNAME  = ALPHA *(nameChar)
+ *
+ *      nameChar  = "-" / "_" / DIGIT / ALPHA
+ *
+ *      subAttr   = "." ATTRNAME
+ *                  ; a sub-attribute of a complex attribute
+ *
+ * See <a href="https://tools.ietf.org/html/rfc7644#section-3.4.2.2">3.4.2.2. Filtering</a>
+ *
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class Filter {
+
+    /**
+     * The filter type.
+     */
+    private final Operator operator;
+
+    /**
+     * The attribute or sub-attribute to filter by, or {@code null} if not
+     * applicable.
+     */
+    private final AttributePath filterAttribute;
+
+    /**
+     * The filter attribute value, or {@code null} if not applicable.
+     */
+    private final String filterValue;
+
+    /**
+     * Specifies whether the filter value is quoted in the string representation
+     * of the filter. String and DateTime values are quoted. Integer and Boolean
+     * values are not quoted.
+     */
+    private final boolean quoteFilterValue;
+
+    /**
+     * The filter components for 'or' and 'and' filter types, or {@code null}
+     * if not applicable.
+     */
+    private final List<Filter> filterComponents;
+
+    /**
+     * Create a new SCIM filter from the provided information.
+     *
+     * @param operator        The filter type.
+     * @param filterAttribute   The attribute or sub-attribute to filter by, or
+     *                          {@code null} if not applicable.
+     * @param filterValue       The filter attribute value, or {@code null} if not
+     *                          applicable.
+     * @param quoteFilterValue  Specifies whether the filter value is quoted in
+     *                          the string representation of the filter.
+     * @param filterComponents  The filter components for 'or' and 'and' filter
+     *                          types, or {@code null} if not applicable.
+     */
+    public Filter(final Operator operator,
+                  final AttributePath filterAttribute,
+                  final String filterValue,
+                  final boolean quoteFilterValue,
+                  final List<Filter> filterComponents) {
+        this.operator = operator;
+        this.filterAttribute   = filterAttribute;
+        this.filterValue       = filterValue;
+        this.quoteFilterValue  = quoteFilterValue;
+        this.filterComponents  = filterComponents;
+    }
+
+    public Operator getOperator() {
+        return operator;
+    }
+
+    public AttributePath getFilterAttribute() {
+        return filterAttribute;
+    }
+
+    public String getFilterValue() {
+        return filterValue;
+    }
+
+    public boolean isQuoteFilterValue() {
+        return quoteFilterValue;
+    }
+
+    public List<Filter> getFilterComponents() {
+        return filterComponents;
+    }
+}
diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/filter/Operator.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/filter/Operator.java
new file mode 100644
index 0000000000..199ea05a96
--- /dev/null
+++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/filter/Operator.java
@@ -0,0 +1,186 @@
+/**
+ * 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.common.scim.filter;
+
+/**
+ *    +----------+-------------+------------------------------------------+
+ *    | Operator | Description | Behavior                                 |
+ *    +----------+-------------+------------------------------------------+
+ *    | eq       | equal       | The attribute and operator values must   |
+ *    |          |             | be identical for a match.                |
+ *    |          |             |                                          |
+ *    | ne       | not equal   | The attribute and operator values are    |
+ *    |          |             | not identical.                           |
+ *    |          |             |                                          |
+ *    | co       | contains    | The entire operator value must be a      |
+ *    |          |             | substring of the attribute value for a   |
+ *    |          |             | match.                                   |
+ *    |          |             |                                          |
+ *    | sw       | starts with | The entire operator value must be a      |
+ *    |          |             | substring of the attribute value,        |
+ *    |          |             | starting at the beginning of the         |
+ *    |          |             | attribute value.  This criterion is      |
+ *    |          |             | satisfied if the two strings are         |
+ *    |          |             | identical.                               |
+ *    |          |             |                                          |
+ *    | ew       | ends with   | The entire operator value must be a      |
+ *    |          |             | substring of the attribute value,        |
+ *    |          |             | matching at the end of the attribute     |
+ *    |          |             | value.  This criterion is satisfied if   |
+ *    |          |             | the two strings are identical.           |
+ *    |          |             |                                          |
+ *    | pr       | present     | If the attribute has a non-empty or      |
+ *    |          | (has value) | non-null value, or if it contains a      |
+ *    |          |             | non-empty node for complex attributes,   |
+ *    |          |             | there is a match.                        |
+ *    |          |             |                                          |
+ *    | gt       | greater     | If the attribute value is greater than   |
+ *    |          | than        | the operator value, there is a match.    |
+ *    |          |             | The actual comparison is dependent on    |
+ *    |          |             | the attribute type.  For string          |
+ *    |          |             | attribute types, this is a               |
+ *    |          |             | lexicographical comparison, and for      |
+ *    |          |             | DateTime types, it is a chronological    |
+ *    |          |             | comparison.  For integer attributes, it  |
+ *    |          |             | is a comparison by numeric value.        |
+ *    |          |             | Boolean and Binary attributes SHALL      |
+ *    |          |             | cause a failed response (HTTP status     |
+ *    |          |             | code 400) with "scimType" of             |
+ *    |          |             | "invalidFilter".                         |
+ *    |          |             |                                          |
+ *    | ge       | greater     | If the attribute value is greater than   |
+ *    |          | than or     | or equal to the operator value, there is |
+ *    |          | equal to    | a match.  The actual comparison is       |
+ *    |          |             | dependent on the attribute type.  For    |
+ *    |          |             | string attribute types, this is a        |
+ *    |          |             | lexicographical comparison, and for      |
+ *    |          |             | DateTime types, it is a chronological    |
+ *    |          |             | comparison.  For integer attributes, it  |
+ *    |          |             | is a comparison by numeric value.        |
+ *    |          |             | Boolean and Binary attributes SHALL      |
+ *    |          |             | cause a failed response (HTTP status     |
+ *    |          |             | code 400) with "scimType" of             |
+ *    |          |             | "invalidFilter".                         |
+ *    |          |             |                                          |
+ *    | lt       | less than   | If the attribute value is less than the  |
+ *    |          |             | operator value, there is a match.  The   |
+ *    |          |             | actual comparison is dependent on the    |
+ *    |          |             | attribute type.  For string attribute    |
+ *    |          |             | types, this is a lexicographical         |
+ *    |          |             | comparison, and for DateTime types, it   |
+ *    |          |             | is a chronological comparison.  For      |
+ *    |          |             | integer attributes, it is a comparison   |
+ *    |          |             | by numeric value.  Boolean and Binary    |
+ *    |          |             | attributes SHALL cause a failed response |
+ *    |          |             | (HTTP status code 400) with "scimType"   |
+ *    |          |             | of "invalidFilter".                      |
+ *    |          |             |                                          |
+ *    | le       | less than   | If the attribute value is less than or   |
+ *    |          | or equal to | equal to the operator value, there is a  |
+ *    |          |             | match.  The actual comparison is         |
+ *    |          |             | dependent on the attribute type.  For    |
+ *    |          |             | string attribute types, this is a        |
+ *    |          |             | lexicographical comparison, and for      |
+ *    |          |             | DateTime types, it is a chronological    |
+ *    |          |             | comparison.  For integer attributes, it  |
+ *    |          |             | is a comparison by numeric value.        |
+ *    |          |             | Boolean and Binary attributes SHALL      |
+ *    |          |             | cause a failed response (HTTP status     |
+ *    |          |             | code 400) with "scimType" of             |
+ *    |          |             | "invalidFilter".                         |
+ *    +----------+-------------+------------------------------------------+
+ *
+ *                        Table 3: Attribute Operators
+ *
+ *    +----------+-------------+------------------------------------------+
+ *    | Operator | Description | Behavior                                 |
+ *    +----------+-------------+------------------------------------------+
+ *    | and      | Logical     | The filter is only a match if both       |
+ *    |          | "and"       | expressions evaluate to true.            |
+ *    |          |             |                                          |
+ *    | or       | Logical     | The filter is a match if either          |
+ *    |          | "or"        | expression evaluates to true.            |
+ *    |          |             |                                          |
+ *    | not      | "Not"       | The filter is a match if the expression  |
+ *    |          | function    | evaluates to false.                      |
+ *    +----------+-------------+------------------------------------------+
+ *
+ *                         Table 4: Logical Operators
+ *
+ *
+ *    +----------+-------------+------------------------------------------+
+ *    | Operator | Description | Behavior                                 |
+ *    +----------+-------------+------------------------------------------+
+ *    | ( )      | Precedence  | Boolean expressions MAY be grouped using |
+ *    |          | grouping    | parentheses to change the standard order |
+ *    |          |             | of operations, i.e., to evaluate logical |
+ *    |          |             | "or" operators before logical "and"      |
+ *    |          |             | operators.                               |
+ *    |          |             |                                          |
+ *    | [ ]      | Complex     | Service providers MAY support complex    |
+ *    |          | attribute   | filters where expressions MUST be        |
+ *    |          | filter      | applied to the same value of a parent    |
+ *    |          | grouping    | attribute specified immediately before   |
+ *    |          |             | the left square bracket ("[").  The      |
+ *    |          |             | expression within square brackets ("["   |
+ *    |          |             | and "]") MUST be a valid filter          |
+ *    |          |             | expression based upon sub-attributes of  |
+ *    |          |             | the parent attribute.  Nested            |
+ *    |          |             | expressions MAY be used.  See examples   |
+ *    |          |             | below.                                   |
+ *    +----------+-------------+------------------------------------------+
+ *
+ *                         Table 5: Grouping Operators
+ *
+ * See <a href="https://tools.ietf.org/html/rfc7644#section-3.4.2.2">3.4.2.2. Filtering</a>
+ *
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public enum Operator {
+
+    AND("and"),
+    OR("or"),
+    EQUALITY("eq"),
+    NOT_EQUAL("ne"),
+    CONTAINS("co"),
+    STARTS_WITH("sw"),
+    ENDS_WITH("ew"),
+    PRESENCE("pr"),
+    GREATER_THAN("gt"),
+    GREATER_OR_EQUAL("ge"),
+    LESS_THAN("lt"),
+    LESS_OR_EQUAL("le");
+
+    private final String value;
+
+    Operator(final String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public static Operator fromString(String value) {
+        for (Operator o: Operator.values()) {
+            if (o.value.equalsIgnoreCase(value)) {
+                return o;
+            }
+        }
+        throw new IllegalArgumentException("No operator with value [" + value + "] found");
+    }
+}
diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/parser/SCIMFilterParser.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/parser/SCIMFilterParser.java
new file mode 100644
index 0000000000..52861ed52b
--- /dev/null
+++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/scim/parser/SCIMFilterParser.java
@@ -0,0 +1,713 @@
+/**
+ * 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.common.scim.parser;
+
+import io.gravitee.am.common.scim.Schema;
+import io.gravitee.am.common.scim.filter.AttributePath;
+import io.gravitee.am.common.scim.filter.Filter;
+import io.gravitee.am.common.scim.filter.Operator;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Stack;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public final class SCIMFilterParser {
+
+    /**
+     * The filter to be parsed.
+     */
+    private final String filterString;
+
+    /**
+     * The default schema that should be assumed when parsing attributes with
+     * no schema explicitly defined in the URN.
+     */
+    private final String defaultSchema;
+
+    /**
+     * The position one higher than the last character.
+     */
+    private int endPos;
+
+    /**
+     * The current character position.
+     */
+    private int currentPos;
+
+    /**
+     * The position marking the first character of the previous word or value.
+     */
+    private int markPos;
+
+    /**
+     * Create a new instance of a filter parser.
+     *
+     * @param filterString  The filter to be parsed.
+     * @param defaultSchema The default schema that should be assumed when parsing
+     *                      attributes without the schema explicitly defined in
+     *                      the URN.
+     */
+    private SCIMFilterParser(final String filterString, final String defaultSchema) {
+        this.filterString = filterString;
+        this.endPos = filterString.length();
+        this.currentPos = 0;
+        this.markPos = 0;
+        this.defaultSchema = defaultSchema;
+    }
+
+    /**
+     * Parse the filter provided in the constructor.
+     *
+     * @return  A parsed SCIM filter.
+     *
+     * @throws  IllegalArgumentException  If the filter string could not be parsed.
+     */
+    public static Filter parse(final String filterString) throws IllegalArgumentException {
+        try {
+            return new SCIMFilterParser(filterString, Schema.SCHEMA_URI_CORE).readFilter();
+        } catch (Exception e) {
+            throw new IllegalArgumentException(MessageFormat.format("Invalid filter ''{0}'': {1}", filterString, e.getMessage()));
+        }
+    }
+
+    /**
+     * Read a filter component at the current position. A filter component is
+     * <pre>
+     * attribute attribute-operator [value]
+     * </pre>
+     * Most attribute operators require a value but 'pr' (presence) requires
+     * no value.
+     *
+     * @return  The parsed filter component.
+     */
+    private Filter readFilterComponent() {
+        String word = readWord();
+        if (word == null)
+        {
+            final String msg = String.format(
+                    "End of input at position %d but expected a filter expression",
+                    markPos);
+            throw new IllegalArgumentException(msg);
+        }
+
+        final AttributePath filterAttribute;
+        try {
+            filterAttribute = AttributePath.parse(word, defaultSchema);
+        } catch (final Exception e) {
+            final String msg = String.format(
+                    "Expected an attribute reference at position %d: %s",
+                    markPos, e.getMessage());
+            throw new IllegalArgumentException(msg);
+        }
+
+        final String operator = readWord();
+        if (operator == null) {
+            final String msg = String.format(
+                    "End of input at position %d but expected an attribute operator",
+                    markPos);
+            throw new IllegalArgumentException(msg);
+        }
+
+        final Operator attributeOperator;
+        try {
+            attributeOperator = Operator.fromString(operator);
+        } catch (Exception ex) {
+            final String msg = String.format(
+                    "Unrecognized attribute operator '%s' at position %d. " +
+                            "Expected: eq,co,sw,pr,gt,ge,lt,le", operator, markPos);
+            throw new IllegalArgumentException(msg);
+        }
+        final Object filterValue;
+        if (!attributeOperator.equals(Operator.PRESENCE)) {
+            filterValue = readValue();
+            if (filterValue == null) {
+                final String msg = String.format(
+                        "End of input at position %d while expecting a value for " +
+                                "operator %s", markPos, operator);
+                throw new IllegalArgumentException(msg);
+            }
+        } else {
+            filterValue = null;
+        }
+
+        final String filterValueString = (filterValue != null) ? filterValue.toString() : null;
+        return new Filter(
+                attributeOperator,
+                filterAttribute,
+                filterValueString,
+                (filterValue != null) && (filterValue instanceof String),
+                null);
+    }
+
+    /**
+     * Read a filter expression.
+     *
+     * @return  The SCIM filter.
+     */
+    private Filter readFilter() {
+        final Stack<Node> expressionStack = new Stack<Node>();
+
+        // Employ the shunting-yard algorithm to parse into reverse polish notation,
+        // where the operands are filter components and the operators are the
+        // logical AND and OR operators. This algorithm ensures that operator
+        // precedence and parentheses are respected.
+        final List<Node> reversePolish = new ArrayList<Node>();
+        for (String word = readWord(); word != null; word = readWord()) {
+            if (word.equalsIgnoreCase("and") || word.equalsIgnoreCase("or")) {
+                final OperatorNode currentOperator;
+                if (word.equalsIgnoreCase("and")) {
+                    currentOperator = new OperatorNode(Operator.AND, markPos);
+                } else {
+                    currentOperator = new OperatorNode(Operator.OR, markPos);
+                }
+                while (!expressionStack.empty() && (expressionStack.peek() instanceof OperatorNode)) {
+                    final OperatorNode previousOperator = (OperatorNode) expressionStack.peek();
+                    if (previousOperator.getPrecedence() < currentOperator.getPrecedence()) {
+                        break;
+                    }
+                    reversePolish.add(expressionStack.pop());
+                }
+                expressionStack.push(currentOperator);
+            } else if (word.equals("(")) {
+                expressionStack.push(new LeftParenthesisNode(markPos));
+            } else if (word.equals(")")) {
+                while (!expressionStack.empty() && !(expressionStack.peek() instanceof LeftParenthesisNode)) {
+                    reversePolish.add(expressionStack.pop());
+                }
+                if (expressionStack.empty()) {
+                    final String msg =
+                            String.format("No opening parenthesis matching closing " +
+                                    "parenthesis at position %d", markPos);
+                    throw new IllegalArgumentException(msg);
+                }
+                expressionStack.pop();
+            } else {
+                rewind();
+                final int pos = currentPos;
+                final Filter filterComponent = readFilterComponent();
+                reversePolish.add(new FilterNode(filterComponent, pos));
+            }
+        }
+
+        while (!expressionStack.empty()) {
+            final Node node = expressionStack.pop();
+            if (node instanceof LeftParenthesisNode) {
+                final String msg =
+                        String.format("No closing parenthesis matching opening " +
+                                "parenthesis at position %d", node.getPos());
+                throw new IllegalArgumentException(msg);
+            }
+            reversePolish.add(node);
+        }
+
+        // Evaluate the reverse polish notation to create a single complex filter.
+        final Stack<FilterNode> filterStack = new Stack<FilterNode>();
+        for (final Node node : reversePolish) {
+            if (node instanceof OperatorNode) {
+                final FilterNode rightOperand = filterStack.pop();
+                final FilterNode leftOperand = filterStack.pop();
+                final OperatorNode operatorNode = (OperatorNode)node;
+                if (operatorNode.getOperator().equals(Operator.AND)) {
+                    final Filter filter = createAndFilter(
+                            Arrays.asList(leftOperand.getFilterComponent(),
+                                    rightOperand.getFilterComponent()));
+                    filterStack.push(new FilterNode(filter, leftOperand.getPos()));
+                } else {
+                    final Filter filter = createOrFilter(
+                            Arrays.asList(leftOperand.getFilterComponent(),
+                                    rightOperand.getFilterComponent()));
+                    filterStack.push(new FilterNode(filter, leftOperand.getPos()));
+                }
+            } else {
+                filterStack.push((FilterNode)node);
+            }
+        }
+
+        if (filterStack.size() == 0) {
+            final String msg = String.format("Empty filter expression");
+            throw new IllegalArgumentException(msg);
+        }
+        else if (filterStack.size() > 1) {
+            final String msg = String.format(
+                    "Unexpected characters at position %d", expressionStack.get(1).pos);
+            throw new IllegalArgumentException(msg);
+        }
+
+        return filterStack.get(0).filterComponent;
+    }
+
+    /**
+     * Read a word at the current position. A word is a consecutive sequence of
+     * characters terminated by whitespace or a parenthesis, or a single opening
+     * or closing parenthesis. Whitespace before and after the word is consumed.
+     * The start of the word is saved in {@code markPos}.
+     *
+     * @return The word at the current position, or {@code null} if the end of
+     *         the input has been reached.
+     */
+    private String readWord() {
+        skipWhitespace();
+        markPos = currentPos;
+
+        loop:
+        while (currentPos < endPos) {
+            final char c = filterString.charAt(currentPos);
+            switch (c) {
+                case '(':
+                case ')':
+                    if (currentPos == markPos) {
+                        currentPos++;
+                    }
+                    break loop;
+                case ' ':
+                    break loop;
+                default:
+                    currentPos++;
+                    break;
+            }
+        }
+
+        if (currentPos - markPos == 0) {
+            return null;
+        }
+
+        final String word = filterString.substring(markPos, currentPos);
+        skipWhitespace();
+        return word;
+    }
+
+    /**
+     * Rewind the current position to the start of the previous word or value.
+     */
+    private void rewind() {
+        currentPos = markPos;
+    }
+
+    /**
+     * Read a value at the current position. A value can be a number, a datetime
+     * or a boolean value (the words true or false), or a string value in double
+     * quotes, using the same syntax as for JSON values. Whitespace before and
+     * after the value is consumed. The start of the value is saved in
+     * {@code markPos}.
+     *
+     * @return An Object representing the value at the current position, or
+     *         {@code null} if the end of the input has already been reached.
+     */
+    public Object readValue() {
+        skipWhitespace();
+        markPos = currentPos;
+
+        if (currentPos == endPos) {
+            return null;
+        }
+
+        if (filterString.charAt(currentPos) == '"') {
+            currentPos++;
+            final StringBuilder builder = new StringBuilder();
+            while (currentPos < endPos) {
+                final char c = filterString.charAt(currentPos);
+                switch (c) {
+                    case '\\':
+                        currentPos++;
+                        if (endOfInput()) {
+                            final String msg = String.format(
+                                    "End of input in a string value that began at " +
+                                            "position %d", markPos);
+                            throw new IllegalArgumentException(msg);
+                        }
+                        final char escapeChar = filterString.charAt(currentPos);
+                        currentPos++;
+                        switch (escapeChar) {
+                            case '"':
+                            case '/':
+                            case '\'':
+                            case '\\':
+                                builder.append(escapeChar);
+                                break;
+                            case 'b':
+                                builder.append('\b');
+                                break;
+                            case 'f':
+                                builder.append('\f');
+                                break;
+                            case 'n':
+                                builder.append('\n');
+                                break;
+                            case 'r':
+                                builder.append('\r');
+                                break;
+                            case 't':
+                                builder.append('\t');
+                                break;
+                            case 'u':
+                                if (currentPos + 4 > endPos)
+                                {
+                                    final String msg = String.format(
+                                            "End of input in a string value that began at " +
+                                                    "position %d", markPos);
+                                    throw new IllegalArgumentException(msg);
+                                }
+                                final String hexChars =
+                                        filterString.substring(currentPos, currentPos + 4);
+                                builder.append((char)Integer.parseInt(hexChars, 16));
+                                currentPos += 4;
+                                break;
+                            default:
+                                final String msg = String.format(
+                                        "Unrecognized escape sequence '\\%c' in a string value " +
+                                                "at position %d", escapeChar, currentPos - 2);
+                                throw new IllegalArgumentException(msg);
+                        }
+                        break;
+
+                    case '"':
+                        currentPos++;
+                        skipWhitespace();
+                        return builder.toString();
+
+                    default:
+                        builder.append(c);
+                        currentPos++;
+                        break;
+                }
+            }
+
+            final String msg = String.format(
+                    "End of input in a string value that began at " +
+                            "position %d", markPos);
+            throw new IllegalArgumentException(msg);
+        } else {
+            loop:
+            while (currentPos < endPos) {
+                final char c = filterString.charAt(currentPos);
+                switch (c) {
+                    case ' ':
+                    case '(':
+                    case ')':
+                        break loop;
+
+                    case '+':
+                    case '-':
+                    case '.':
+                    case '0':
+                    case '1':
+                    case '2':
+                    case '3':
+                    case '4':
+                    case '5':
+                    case '6':
+                    case '7':
+                    case '8':
+                    case '9':
+                    case 'A':
+                    case 'B':
+                    case 'C':
+                    case 'D':
+                    case 'E':
+                    case 'F':
+                    case 'G':
+                    case 'H':
+                    case 'I':
+                    case 'J':
+                    case 'K':
+                    case 'L':
+                    case 'M':
+                    case 'N':
+                    case 'O':
+                    case 'P':
+                    case 'Q':
+                    case 'R':
+                    case 'S':
+                    case 'T':
+                    case 'U':
+                    case 'V':
+                    case 'W':
+                    case 'X':
+                    case 'Y':
+                    case 'Z':
+                    case 'a':
+                    case 'b':
+                    case 'c':
+                    case 'd':
+                    case 'e':
+                    case 'f':
+                    case 'g':
+                    case 'h':
+                    case 'i':
+                    case 'j':
+                    case 'k':
+                    case 'l':
+                    case 'm':
+                    case 'n':
+                    case 'o':
+                    case 'p':
+                    case 'q':
+                    case 'r':
+                    case 's':
+                    case 't':
+                    case 'u':
+                    case 'v':
+                    case 'w':
+                    case 'x':
+                    case 'y':
+                    case 'z':
+                        // These are all OK.
+                        currentPos++;
+                        break;
+
+                    case '/':
+                    case ':':
+                    case ';':
+                    case '<':
+                    case '=':
+                    case '>':
+                    case '?':
+                    case '@':
+                    case '[':
+                    case '\\':
+                    case ']':
+                    case '^':
+                    case '_':
+                    case '`':
+                        // These are not allowed, but they are explicitly called out because
+                        // they are included in the range of values between '-' and 'z', and
+                        // making sure all possible characters are included can help make
+                        // the switch statement more efficient.  We'll fall through to the
+                        // default clause to reject them.
+                    default:
+                        final String msg = String.format(
+                                "Invalid character '%c' in a number or boolean value at " +
+                                        "position %d",
+                                c, currentPos);
+                        throw new IllegalArgumentException(msg);
+                }
+            }
+
+            final String s = filterString.substring(markPos, currentPos);
+            skipWhitespace();
+            final Object value = stringToValue(s);
+            if (value == null || value instanceof String) {
+                final String msg = String.format(
+                        "Invalid filter value beginning at position %d", markPos);
+                throw new IllegalArgumentException(msg);
+            }
+            return value;
+        }
+    }
+
+    /**
+     * Determine if the end of the input has been reached.
+     *
+     * @return  {@code true} if the end of the input has been reached.
+     */
+    private boolean endOfInput() {
+        return currentPos == endPos;
+    }
+
+    /**
+     * Skip over any whitespace at the current position.
+     */
+    private void skipWhitespace() {
+        while (currentPos < endPos && filterString.charAt(currentPos) == ' ') {
+            currentPos++;
+        }
+    }
+
+    private static Object stringToValue(String string) {
+        if (string.equals("")) {
+            return string;
+        } else if (string.equalsIgnoreCase("true")) {
+            return Boolean.TRUE;
+        } else if (string.equalsIgnoreCase("false")) {
+            return Boolean.FALSE;
+        } else if (string.equalsIgnoreCase("null")) {
+            return null;
+        } else {
+            char initial = string.charAt(0);
+            if (initial >= '0' && initial <= '9' || initial == '-') {
+                try {
+                    if (isDecimalNotation(string)) {
+                        Double d = Double.valueOf(string);
+                        if (!d.isInfinite() && !d.isNaN()) {
+                            return d;
+                        }
+                    } else {
+                        Long myLong = Long.valueOf(string);
+                        if (string.equals(myLong.toString())) {
+                            if (myLong == (long)myLong.intValue()) {
+                                return myLong.intValue();
+                            }
+
+                            return myLong;
+                        }
+                    }
+                } catch (Exception var3) {
+                }
+            }
+
+            return string;
+        }
+    }
+
+    private static boolean isDecimalNotation(String val) {
+        return val.indexOf(46) > -1 || val.indexOf(101) > -1 || val.indexOf(69) > -1 || "-0".equals(val);
+    }
+
+    private static Filter createAndFilter(final List<Filter> filterComponents) {
+        return new Filter(Operator.AND, null, null, false, new ArrayList<>(filterComponents));
+    }
+
+    private static Filter createOrFilter(final List<Filter> filterComponents) {
+        return new Filter(Operator.OR, null, null, false, new ArrayList<>(filterComponents));
+    }
+
+    /**
+     * Base class for expression stack nodes. The expression stack is needed to
+     * employ the shunting-yard algorithm to parse the filter expression.
+     */
+    class Node {
+        private final int pos;
+
+        /**
+         * Create a new node.
+         *
+         * @param pos  The position of the node in the filter string.
+         */
+        public Node(final int pos)
+        {
+            this.pos = pos;
+        }
+
+        /**
+         * Retrieve the position of the node in the filter string.
+         * @return  The position of the node in the filter string.
+         */
+        public int getPos()
+        {
+            return pos;
+        }
+    }
+
+    /**
+     * A node representing a filter component.
+     */
+    class FilterNode extends Node {
+        private final Filter filterComponent;
+
+        /**
+         * Create a new filter component node.
+         *
+         * @param filterComponent  The filter component.
+         * @param pos              The position of the node in the filter string.
+         */
+        public FilterNode(final Filter filterComponent,
+                          final int pos) {
+            super(pos);
+            this.filterComponent = filterComponent;
+        }
+
+        /**
+         * Retrieve the filter component.
+         *
+         * @return  The filter component.
+         */
+        public Filter getFilterComponent()
+        {
+            return filterComponent;
+        }
+
+        @Override
+        public String toString() {
+            return "FilterNode{" +
+                    "filterComponent=" + filterComponent +
+                    "} " + super.toString();
+        }
+    }
+
+    /**
+     * A node representing a logical operator.
+     */
+    class OperatorNode extends Node {
+        private final Operator operator;
+
+        /**
+         * Create a new logical operator node.
+         *
+         * @param operator   The type of operator, either SCIMFilterType.AND or
+         *                     SCIMFilterType.OR.
+         * @param pos          The position of the node in the filter string.
+         */
+        public OperatorNode(final Operator operator,
+                            final int pos) {
+            super(pos);
+            this.operator = operator;
+        }
+
+        /**
+         * Retrieve the type of operator.
+         *
+         * @return  The type of operator, either SCIMFilterType.AND or
+         *          SCIMFilterType.OR.
+         */
+        public Operator getOperator()
+        {
+            return operator;
+        }
+
+        /**
+         * Retrieve the precedence of the operator.
+         *
+         * @return  The precedence of the operator.
+         */
+        public int getPrecedence() {
+            switch (operator) {
+                case AND:
+                    return 2;
+                case OR:
+                default:
+                    return 1;
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "OperatorNode{" +
+                    "operator=" + operator +
+                    "} " + super.toString();
+        }
+    }
+
+    /**
+     * A node representing an opening parenthesis.
+     */
+    class LeftParenthesisNode extends Node {
+        /**
+         * Create a new opening parenthesis node.
+         *
+         * @param pos  The position of the parenthesis in the filter string.
+         */
+        public LeftParenthesisNode(final int pos)
+        {
+            super(pos);
+        }
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/resources/users/UsersEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/resources/users/UsersEndpoint.java
index f891bfcbf1..21de969470 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/resources/users/UsersEndpoint.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/resources/users/UsersEndpoint.java
@@ -16,6 +16,8 @@
 package io.gravitee.am.gateway.handler.scim.resources.users;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import io.gravitee.am.common.scim.filter.Filter;
+import io.gravitee.am.common.scim.parser.SCIMFilterParser;
 import io.gravitee.am.gateway.handler.scim.exception.InvalidSyntaxException;
 import io.gravitee.am.gateway.handler.scim.exception.InvalidValueException;
 import io.gravitee.am.gateway.handler.scim.model.User;
@@ -44,7 +46,7 @@ public class UsersEndpoint extends AbstractUserEndpoint {
         // Pagination (https://tools.ietf.org/html/rfc7644#section-3.4.2.4)
         Integer page = DEFAULT_START_INDEX;
         Integer size = MAX_ITEMS_PER_PAGE;
-
+        Filter filter = null;
 
         // The 1-based index of the first query result.
         // A value less than 1 SHALL be interpreted as 1.
@@ -63,8 +65,19 @@ public class UsersEndpoint extends AbstractUserEndpoint {
 
         }
 
+        // Filter results
+        final String filterParam = context.request().getParam("filter");
+        if (filterParam != null && !filterParam.isEmpty()) {
+            try {
+                filter = SCIMFilterParser.parse(filterParam);
+            } catch (Exception ex) {
+                context.fail(new InvalidSyntaxException(ex.getMessage()));
+                return;
+            }
+        }
+
         // user service use 0-based index
-        userService.list(page - 1, size, location(context.request()))
+        userService.list(filter, page - 1, size, location(context.request()))
                 .subscribe(
                         users -> context.response()
                                 .putHeader(HttpHeaders.CACHE_CONTROL, "no-store")
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/UserService.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/UserService.java
index 1d2a12d120..9b9a17ef8f 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/UserService.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/UserService.java
@@ -17,6 +17,7 @@ package io.gravitee.am.gateway.handler.scim.service;
 
 import io.gravitee.am.gateway.handler.scim.model.ListResponse;
 import io.gravitee.am.gateway.handler.scim.model.User;
+import io.gravitee.am.common.scim.filter.Filter;
 import io.reactivex.Completable;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
@@ -27,7 +28,7 @@ import io.reactivex.Single;
  */
 public interface UserService {
 
-    Single<ListResponse<User>> list(int page, int size, String baseUrl);
+    Single<ListResponse<User>> list(Filter filter, int page, int size, String baseUrl);
 
     Maybe<User> get(String userId, String baseUrl);
 
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/impl/ServiceProviderConfigServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/impl/ServiceProviderConfigServiceImpl.java
index 7bbbc87185..962848b0e8 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/impl/ServiceProviderConfigServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/impl/ServiceProviderConfigServiceImpl.java
@@ -34,7 +34,7 @@ public class ServiceProviderConfigServiceImpl implements ServiceProviderConfigSe
         ServiceProviderConfiguration serviceProviderConfiguration = new ServiceProviderConfiguration();
         serviceProviderConfiguration.setPatch(new ComplexType(false));
         serviceProviderConfiguration.setBulk(new ComplexType(false));
-        serviceProviderConfiguration.setFilter(new ComplexType(false));
+        serviceProviderConfiguration.setFilter(new ComplexType(true));
         serviceProviderConfiguration.setChangePassword(new ComplexType(false));
         serviceProviderConfiguration.setSort(new ComplexType(false));
         serviceProviderConfiguration.setEtag(new ComplexType(false));
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/impl/UserServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/impl/UserServiceImpl.java
index 2d7b5a1061..e8538a87d1 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/impl/UserServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/main/java/io/gravitee/am/gateway/handler/scim/service/impl/UserServiceImpl.java
@@ -16,6 +16,7 @@
 package io.gravitee.am.gateway.handler.scim.service.impl;
 
 import io.gravitee.am.common.oidc.StandardClaims;
+import io.gravitee.am.common.scim.filter.Filter;
 import io.gravitee.am.common.utils.RandomString;
 import io.gravitee.am.gateway.handler.common.auth.idp.IdentityProviderManager;
 import io.gravitee.am.gateway.handler.scim.exception.InvalidValueException;
@@ -26,9 +27,11 @@ import io.gravitee.am.gateway.handler.scim.service.GroupService;
 import io.gravitee.am.gateway.handler.scim.service.UserService;
 import io.gravitee.am.identityprovider.api.DefaultUser;
 import io.gravitee.am.model.Domain;
-import io.gravitee.am.model.Role;
 import io.gravitee.am.model.ReferenceType;
+import io.gravitee.am.model.Role;
+import io.gravitee.am.model.common.Page;
 import io.gravitee.am.repository.management.api.UserRepository;
+import io.gravitee.am.repository.management.api.search.FilterCriteria;
 import io.gravitee.am.service.RoleService;
 import io.gravitee.am.service.exception.*;
 import io.gravitee.am.service.validators.UserValidator;
@@ -68,9 +71,13 @@ public class UserServiceImpl implements UserService {
     private IdentityProviderManager identityProviderManager;
 
     @Override
-    public Single<ListResponse<User>> list(int page, int size, String baseUrl) {
+    public Single<ListResponse<User>> list(Filter filter, int page, int size, String baseUrl) {
         LOGGER.debug("Find users by domain: {}", domain.getId());
-        return userRepository.findByDomain(domain.getId(), page, size)
+        Single<Page<io.gravitee.am.model.User>> findUsers = filter != null ?
+                userRepository.search(ReferenceType.DOMAIN, domain.getId(), FilterCriteria.convert(filter), page, size) :
+                userRepository.findByDomain(domain.getId(), page, size);
+
+        return findUsers
                 .flatMap(userPage -> {
                     // A negative value SHALL be interpreted as "0".
                     // A value of "0" indicates that no resource results are to be returned except for "totalResults".
@@ -87,8 +94,8 @@ public class UserServiceImpl implements UserService {
                     }
                 })
                 .onErrorResumeNext(ex -> {
-                    LOGGER.error("An error occurs while trying to find users by domain {}", domain, ex);
-                    return Single.error(new TechnicalManagementException(String.format("An error occurs while trying to find users by domain %s", domain), ex));
+                    LOGGER.error("An error occurs while trying to find users for the security domain {}", domain.getName(), ex);
+                    return Single.error(new TechnicalManagementException(String.format("An error occurs while trying to find users the security domain %s", domain.getName()), ex));
                 });
     }
 
@@ -312,7 +319,7 @@ public class UserServiceImpl implements UserService {
         User scimUser = new User();
         scimUser.setSchemas(User.SCHEMAS);
         scimUser.setId(user.getId());
-        scimUser.setExternalId(get(additionalInformation, StandardClaims.SUB, String.class));
+        scimUser.setExternalId(user.getExternalId());
         scimUser.setUserName(user.getUsername());
 
         Name name = new Name();
diff --git a/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/AbstractUsersResource.java b/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/AbstractUsersResource.java
new file mode 100644
index 0000000000..6b5a367887
--- /dev/null
+++ b/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/AbstractUsersResource.java
@@ -0,0 +1,64 @@
+/**
+ * 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.common.scim.parser.SCIMFilterParser;
+import io.gravitee.am.management.service.UserService;
+import io.gravitee.am.model.ReferenceType;
+import io.gravitee.am.model.User;
+import io.gravitee.am.model.common.Page;
+import io.gravitee.am.repository.management.api.search.FilterCriteria;
+import io.reactivex.Single;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.ws.rs.BadRequestException;
+
+/**
+ * @author Titouan COMPIEGNE (titouuan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public abstract class AbstractUsersResource extends AbstractResource {
+
+    protected static final int MAX_USERS_SIZE_PER_PAGE = 30;
+    protected static final String MAX_USERS_SIZE_PER_PAGE_STRING = "30";
+
+    @Autowired
+    protected UserService userService;
+
+    protected Single<Page<User>> searchUsers(ReferenceType referenceType,
+                                             String referenceId,
+                                             String query,
+                                             String filter,
+                                             int page,
+                                             int size) {
+        if (query != null) {
+            return userService.search(referenceType, referenceId, query, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
+        }
+        if (filter != null) {
+            return Single.defer(() -> {
+                FilterCriteria filterCriteria = FilterCriteria.convert(SCIMFilterParser.parse(filter));
+                return userService.search(referenceType, referenceId, filterCriteria, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
+            }).onErrorResumeNext(ex -> {
+                if (ex instanceof IllegalArgumentException) {
+                    return Single.error(new BadRequestException(ex.getMessage()));
+                }
+                return Single.error(ex);
+            });
+        }
+        return userService.findAll(referenceType, referenceId, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
+    }
+
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/environments/domains/UsersResource.java b/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/environments/domains/UsersResource.java
index 000c211e5a..e06ab94641 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/environments/domains/UsersResource.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/environments/domains/UsersResource.java
@@ -15,9 +15,9 @@
  */
 package io.gravitee.am.management.handlers.management.api.resources.organizations.environments.domains;
 
-import io.gravitee.am.management.handlers.management.api.resources.AbstractResource;
-import io.gravitee.am.management.service.UserService;
+import io.gravitee.am.management.handlers.management.api.resources.AbstractUsersResource;
 import io.gravitee.am.model.Acl;
+import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.model.User;
 import io.gravitee.am.model.common.Page;
 import io.gravitee.am.model.permissions.Permission;
@@ -50,17 +50,11 @@ import java.util.Comparator;
  * @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";
+public class UsersResource extends AbstractUsersResource {
 
     @Context
     private ResourceContext resourceContext;
 
-    @Autowired
-    private UserService userService;
-
     @Autowired
     private DomainService domainService;
 
@@ -86,6 +80,7 @@ public class UsersResource extends AbstractResource {
             @PathParam("environmentId") String environmentId,
             @PathParam("domain") String domain,
             @QueryParam("q") String query,
+            @QueryParam("filter") String filter,
             @QueryParam("page") @DefaultValue("0") int page,
             @QueryParam("size") @DefaultValue(MAX_USERS_SIZE_PER_PAGE_STRING) int size,
             @Suspended final AsyncResponse response) {
@@ -95,13 +90,7 @@ public class UsersResource extends AbstractResource {
         checkAnyPermission(organizationId, environmentId, domain, Permission.DOMAIN_USER, Acl.LIST)
                 .andThen(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));
-                            }
-                        })
+                        .flatMapSingle(__ -> searchUsers(ReferenceType.DOMAIN, domain, query, filter, page, size))
                         .flatMap(pagedUsers ->
                                 hasAnyPermission(authenticatedUser, organizationId, environmentId, domain, Permission.DOMAIN_USER, Acl.READ)
                                         .flatMap(hasPermission -> Observable.fromIterable(pagedUsers.getData())
diff --git a/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/users/UsersResource.java b/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/users/UsersResource.java
index e3cac3f085..007319ed36 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/users/UsersResource.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/users/UsersResource.java
@@ -15,8 +15,7 @@
  */
 package io.gravitee.am.management.handlers.management.api.resources.organizations.users;
 
-import io.gravitee.am.management.handlers.management.api.resources.AbstractResource;
-import io.gravitee.am.management.service.UserService;
+import io.gravitee.am.management.handlers.management.api.resources.AbstractUsersResource;
 import io.gravitee.am.model.Acl;
 import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.model.User;
@@ -50,17 +49,11 @@ import java.util.Set;
  * @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";
+public class UsersResource extends AbstractUsersResource {
 
     @Context
     private ResourceContext resourceContext;
 
-    @Autowired
-    private UserService userService;
-
     @Autowired
     private IdentityProviderService identityProviderService;
 
@@ -79,24 +72,19 @@ public class UsersResource extends AbstractResource {
     public void list(
             @PathParam("organizationId") String organizationId,
             @QueryParam("q") String query,
+            @QueryParam("filter") String filter,
             @QueryParam("page") @DefaultValue("0") int page,
             @QueryParam("size") @DefaultValue(MAX_USERS_SIZE_PER_PAGE_STRING) int size,
             @Suspended final AsyncResponse response) {
 
         io.gravitee.am.identityprovider.api.User authenticatedUser = getAuthenticatedUser();
-        final Single<Page<User>> usersPageObs;
-
-        if (query != null) {
-            usersPageObs = userService.search(ReferenceType.ORGANIZATION, organizationId, query, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
-        } else {
-            usersPageObs = userService.findAll(ReferenceType.ORGANIZATION, organizationId, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
-        }
 
         permissionService.findAllPermissions(authenticatedUser, ReferenceType.ORGANIZATION, organizationId)
                 .flatMap(organizationPermissions ->
                         checkPermission(organizationPermissions, Permission.ORGANIZATION_USER, Acl.LIST)
-                                .andThen(usersPageObs.flatMap(pagedUsers ->
-                                        Observable.fromIterable(pagedUsers.getData())
+                                .andThen(searchUsers(ReferenceType.DOMAIN, organizationId, query, filter, page, size)
+                                        .flatMap(pagedUsers ->
+                                                Observable.fromIterable(pagedUsers.getData())
                                                 .flatMapSingle(user -> filterUserInfos(organizationPermissions, user))
                                                 .toSortedList(Comparator.comparing(User::getUsername))
                                                 .map(users -> new Page<>(users, pagedUsers.getCurrentPage(), pagedUsers.getTotalCount())))))
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 8f339f8458..2c95540d4c 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
@@ -19,6 +19,7 @@ import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.model.User;
 import io.gravitee.am.model.common.Page;
 import io.gravitee.am.model.factor.EnrolledFactor;
+import io.gravitee.am.repository.management.api.search.FilterCriteria;
 import io.gravitee.am.service.model.NewUser;
 import io.gravitee.am.service.model.UpdateUser;
 import io.reactivex.Completable;
@@ -35,7 +36,7 @@ public interface UserService {
 
     Single<Page<User>> search(ReferenceType referenceType, String referenceId, String query, int page, int size);
 
-    Single<Page<User>> search(String domain, String query, int page, int size);
+    Single<Page<User>> search(ReferenceType referenceType, String referenceId, FilterCriteria filterCriteria, int page, int size);
 
     Single<Page<User>> findAll(ReferenceType referenceType, String referenceId, 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 402e771987..fa00cdfd74 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
@@ -29,6 +29,7 @@ import io.gravitee.am.model.*;
 import io.gravitee.am.model.account.AccountSettings;
 import io.gravitee.am.model.common.Page;
 import io.gravitee.am.model.factor.EnrolledFactor;
+import io.gravitee.am.repository.management.api.search.FilterCriteria;
 import io.gravitee.am.repository.management.api.search.LoginAttemptCriteria;
 import io.gravitee.am.service.*;
 import io.gravitee.am.service.exception.*;
@@ -60,9 +61,6 @@ public class UserServiceImpl implements UserService {
     @Value("${user.registration.token.expire-after:86400}")
     private Integer expireAfter;
 
-    @Value("${gateway.url:http://localhost:8092}")
-    private String gatewayUrl;
-
     @Autowired
     private io.gravitee.am.service.UserService userService;
 
@@ -97,8 +95,8 @@ public class UserServiceImpl implements UserService {
     }
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int page, int size) {
-        return search(ReferenceType.DOMAIN, domain, query, page, size);
+    public Single<Page<User>> search(ReferenceType referenceType, String referenceId, FilterCriteria filterCriteria, int page, int size) {
+        return userService.search(referenceType, referenceId, filterCriteria, 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 111c04efd6..1701450234 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
@@ -15,11 +15,12 @@
  */
 package io.gravitee.am.repository.management.api;
 
+import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.model.User;
 import io.gravitee.am.model.analytics.AnalyticsQuery;
 import io.gravitee.am.model.common.Page;
-import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.repository.common.CrudRepository;
+import io.gravitee.am.repository.management.api.search.FilterCriteria;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
 
@@ -41,7 +42,7 @@ public interface UserRepository extends CrudRepository<User, String> {
 
     Single<Page<User>> search(ReferenceType referenceType, String referenceId, String query, int page, int size);
 
-    Single<Page<User>> search(String domain, String query, int page, int size);
+    Single<Page<User>> search(ReferenceType referenceType, String referenceId, FilterCriteria criteria, int page, int size);
 
     Single<List<User>> findByDomainAndEmail(String domain, String email, boolean strict);
 
diff --git a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/search/FilterCriteria.java b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/search/FilterCriteria.java
new file mode 100644
index 0000000000..cf3622480d
--- /dev/null
+++ b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/search/FilterCriteria.java
@@ -0,0 +1,115 @@
+/**
+ * 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.repository.management.api.search;
+
+import io.gravitee.am.common.scim.Schema;
+import io.gravitee.am.common.scim.filter.Filter;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class FilterCriteria {
+
+    private String operator;
+    private String filterName;
+    private String filterValue;
+    private boolean quoteFilterValue;
+    private List<FilterCriteria> filterComponents;
+
+    public FilterCriteria() { }
+
+    public FilterCriteria(String operator, String filterName, String filterValue, boolean quoteFilterValue, List<FilterCriteria> filterComponents) {
+        this.operator = operator;
+        this.filterName = filterName;
+        this.filterValue = filterValue;
+        this.quoteFilterValue = quoteFilterValue;
+        this.filterComponents = filterComponents;
+    }
+
+    public String getOperator() {
+        return operator;
+    }
+
+    public void setOperator(String operator) {
+        this.operator = operator;
+    }
+
+    public String getFilterName() {
+        return filterName;
+    }
+
+    public void setFilterName(String filterName) {
+        this.filterName = filterName;
+    }
+
+    public String getFilterValue() {
+        return filterValue;
+    }
+
+    public void setFilterValue(String filterValue) {
+        this.filterValue = filterValue;
+    }
+
+    public boolean isQuoteFilterValue() {
+        return quoteFilterValue;
+    }
+
+    public void setQuoteFilterValue(boolean quoteFilterValue) {
+        this.quoteFilterValue = quoteFilterValue;
+    }
+
+    public List<FilterCriteria> getFilterComponents() {
+        return filterComponents;
+    }
+
+    public void setFilterComponents(List<FilterCriteria> filterComponents) {
+        this.filterComponents = filterComponents;
+    }
+
+    public static FilterCriteria convert(Filter scimFilter) {
+        if (scimFilter == null) {
+            return null;
+        }
+        String filterAttribute = scimFilter.getFilterAttribute() != null ?
+                scimFilter.getFilterAttribute()
+                        .toString()
+                        .replace(Schema.SCHEMA_URI_USER + ":", "")
+                        .replace(Schema.SCHEMA_URI_GROUP + ":", "") : null;
+        FilterCriteria filterCriteria = new FilterCriteria(
+                scimFilter.getOperator().getValue(),
+                filterAttribute,
+                scimFilter.getFilterValue(),
+                scimFilter.isQuoteFilterValue(),
+                scimFilter.getFilterComponents() != null ? scimFilter.getFilterComponents().stream().map(FilterCriteria::convert).collect(Collectors.toList()) : null);
+        return filterCriteria;
+    }
+
+    @Override
+    public String toString() {
+        return "{\"_class\":\"FilterCriteria\", " +
+                "\"operator\":" + (operator == null ? "null" : "\"" + operator + "\"") + ", " +
+                "\"filterName\":" + (filterName == null ? "null" : "\"" + filterName + "\"") + ", " +
+                "\"filterValue\":" + (filterValue == null ? "null" : "\"" + filterValue + "\"") + ", " +
+                "\"quoteFilterValue\":\"" + quoteFilterValue + "\"" + ", " +
+                "\"filterComponents\":" + (filterComponents == null ? "null" : Arrays.toString(filterComponents.toArray())) +
+                "}";
+    }
+}
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/common/FilterCriteriaParser.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/common/FilterCriteriaParser.java
new file mode 100644
index 0000000000..4346da2c66
--- /dev/null
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/common/FilterCriteriaParser.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.repository.mongodb.common;
+
+import io.gravitee.am.repository.management.api.search.FilterCriteria;
+
+/**
+ * @author Titouan COMPIEGNE (david.brassely at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public final class FilterCriteriaParser {
+
+    private FilterCriteriaParser() {}
+
+    public static String parse(FilterCriteria criteria) {
+        return parse(criteria, new StringBuilder());
+    }
+
+    private static String parse(FilterCriteria criteria, final StringBuilder builder) {
+        if (criteria.getFilterComponents() != null) {
+            builder.append("{");
+            builder.append(convertOperator(criteria.getOperator()));
+            builder.append(":");
+            builder.append("[");
+            int size = criteria.getFilterComponents().size();
+            for (int i = 0; i < size; ++i) {
+                parse(criteria.getFilterComponents().get(i), builder);
+                if (i < size - 1) {
+                    builder.append(",");
+                }
+            }
+            builder.append("]");
+        } else {
+            String operator = convertOperator(criteria.getOperator());
+            String filterName = convertFilterName(criteria.getFilterName());
+            builder.append("{");
+            builder.append("\"" + filterName + "\"");
+            builder.append(":");
+            builder.append("{");
+            builder.append(operator);
+            builder.append(":");
+            if ("$exists".equals(operator)) {
+                builder.append(true);
+            } else {
+                builder.append(convertFilterValue(criteria, filterName, criteria.getOperator()));
+            }
+            if ("$regex".equals(operator)) {
+                builder.append(",$options:\"i\"");
+            }
+            builder.append("}");
+        }
+        builder.append("}");
+        return builder.toString();
+    }
+
+    private static String convertOperator(String operator) {
+        if (operator == null) {
+            return null;
+        }
+
+        switch (operator) {
+            case "or":
+                return "$or";
+            case "and":
+                return "$and";
+            case "eq":
+                return "$eq";
+            case "ne":
+                return "$ne";
+            case "gt":
+                return "$gt";
+            case "ge":
+                return "$gte";
+            case "lt":
+                return "$lt";
+            case "le":
+                return "$lte";
+            case "pr":
+                return "$exists";
+            case "co":
+            case "sw":
+            case "ew":
+                return "$regex";
+            default:
+                return operator;
+        }
+    }
+
+    private static String convertFilterName(String filterName) {
+        if (filterName == null) {
+            return null;
+        }
+
+        switch (filterName) {
+            case "id":
+                return "_id";
+            case "userName":
+                return "username";
+            case "name.familyName":
+                return "additionalInformation.family_name";
+            case "name.givenName":
+                return "additionalInformation.given_name";
+            case "name.middleName":
+                return "additionalInformation.middle_name";
+            case "meta.created":
+                return "createdAt";
+            case "meta.lastModified":
+                return "updatedAt";
+            case "profileUrl":
+                return "additionalInformation.profile";
+            case "locale":
+                return "additionalInformation.locale";
+            case "timezone":
+                return "additionalInformation.zoneinfo";
+            case "active":
+                return "enabled";
+            case "emails.value":
+                return "email";
+            default:
+                return filterName;
+        }
+    }
+
+    private static String convertFilterValue(FilterCriteria criteria, String filterName, String operator) {
+        String filterValue = criteria.getFilterValue();
+        if (isDateInput(filterName)) {
+            filterValue = "ISODate(\"" + filterValue + "\")";
+            return filterValue;
+        }
+        if ("sw".equals(operator)) {
+            filterValue = "^" + filterValue;
+        }
+        if ("ew".equals(operator)) {
+            filterValue += "$";
+        }
+        if (criteria.isQuoteFilterValue()) {
+            filterValue = "\"" + filterValue + "\"";
+        }
+        return filterValue;
+    }
+
+    private static boolean isDateInput(String filterName) {
+        return "createdAt".equals(filterName) ||
+                "updatedAt".equals(filterName);
+    }
+}
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/common/MongoFactory.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/common/MongoFactory.java
index 9b9cb4fc45..3e26dcdc2c 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/common/MongoFactory.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/common/MongoFactory.java
@@ -64,7 +64,6 @@ public class MongoFactory implements FactoryBean<MongoClient> {
 
         CodecRegistry defaultCodecRegistry = MongoClients.getDefaultCodecRegistry();
         CodecRegistry pojoCodecRegistry = fromProviders(PojoCodecProvider.builder()
-                .register("io.gravitee.am.repository.mongodb.model", "io.gravitee.am.model")
                 .automatic(true)
                 .build());
 
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 130f4cc2e4..e99b9ac77b 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
@@ -21,14 +21,17 @@ import com.mongodb.client.model.Aggregates;
 import com.mongodb.reactivestreams.client.MongoCollection;
 import io.gravitee.am.common.analytics.Field;
 import io.gravitee.am.common.utils.RandomString;
+import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.model.User;
 import io.gravitee.am.model.analytics.AnalyticsQuery;
 import io.gravitee.am.model.common.Page;
-import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.model.scim.Address;
 import io.gravitee.am.model.scim.Attribute;
 import io.gravitee.am.model.scim.Certificate;
+import io.gravitee.am.repository.exceptions.TechnicalException;
 import io.gravitee.am.repository.management.api.UserRepository;
+import io.gravitee.am.repository.management.api.search.FilterCriteria;
+import io.gravitee.am.repository.mongodb.common.FilterCriteriaParser;
 import io.gravitee.am.repository.mongodb.management.internal.model.UserMongo;
 import io.gravitee.am.repository.mongodb.management.internal.model.scim.AddressMongo;
 import io.gravitee.am.repository.mongodb.management.internal.model.scim.AttributeMongo;
@@ -39,6 +42,8 @@ import io.reactivex.Observable;
 import io.reactivex.Single;
 import org.bson.Document;
 import org.bson.conversions.Bson;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.PostConstruct;
@@ -58,8 +63,12 @@ import static io.gravitee.am.model.ReferenceType.DOMAIN;
 @Component
 public class MongoUserRepository extends AbstractManagementMongoRepository implements UserRepository {
 
+    private static final Logger logger = LoggerFactory.getLogger(MongoUserRepository.class);
     private static final String FIELD_ID = "_id";
     private static final String FIELD_USERNAME = "username";
+    private static final String FIELD_DISPLAY_NAME = "displayName";
+    private static final String FIELD_FIRST_NAME = "firstName";
+    private static final String FIELD_LAST_NAME = "lastName";
     private static final String FIELD_SOURCE = "source";
     private static final String FIELD_EMAIL = "email";
     private static final String FIELD_EMAIL_CLAIM = "additionalInformation.email";
@@ -74,7 +83,11 @@ public class MongoUserRepository extends AbstractManagementMongoRepository imple
 
         super.createIndex(usersCollection, new Document(FIELD_REFERENCE_TYPE, 1).append(FIELD_REFERENCE_ID, 1));
         super.createIndex(usersCollection, new Document(FIELD_REFERENCE_TYPE, 1).append(FIELD_REFERENCE_ID, 1).append(FIELD_EMAIL, 1));
+        super.createIndex(usersCollection, new Document(FIELD_REFERENCE_TYPE, 1).append(FIELD_REFERENCE_ID, 1).append(FIELD_EMAIL_CLAIM, 1));
         super.createIndex(usersCollection, new Document(FIELD_REFERENCE_TYPE, 1).append(FIELD_REFERENCE_ID, 1).append(FIELD_USERNAME, 1));
+        super.createIndex(usersCollection, new Document(FIELD_REFERENCE_TYPE, 1).append(FIELD_REFERENCE_ID, 1).append(FIELD_DISPLAY_NAME, 1));
+        super.createIndex(usersCollection, new Document(FIELD_REFERENCE_TYPE, 1).append(FIELD_REFERENCE_ID, 1).append(FIELD_FIRST_NAME, 1));
+        super.createIndex(usersCollection, new Document(FIELD_REFERENCE_TYPE, 1).append(FIELD_REFERENCE_ID, 1).append(FIELD_LAST_NAME, 1));
         super.createIndex(usersCollection, new Document(FIELD_REFERENCE_TYPE, 1).append(FIELD_REFERENCE_ID, 1).append(FIELD_EXTERNAL_ID, 1));
         super.createIndex(usersCollection, new Document(FIELD_REFERENCE_TYPE, 1).append(FIELD_REFERENCE_ID, 1).append(FIELD_USERNAME, 1).append(FIELD_SOURCE, 1));
         super.createIndex(usersCollection, new Document(FIELD_REFERENCE_TYPE, 1).append(FIELD_REFERENCE_ID, 1).append(FIELD_EXTERNAL_ID, 1).append(FIELD_SOURCE, 1));
@@ -99,13 +112,26 @@ public class MongoUserRepository extends AbstractManagementMongoRepository imple
 
     @Override
     public Single<Page<User>> search(ReferenceType referenceType, String referenceId, String query, int page, int size) {
-        // currently search on username field
-        Bson searchQuery = new BasicDBObject(FIELD_USERNAME, query);
+        Bson searchQuery = or(
+                new BasicDBObject(FIELD_USERNAME, query),
+                new BasicDBObject(FIELD_EMAIL, query),
+                new BasicDBObject(FIELD_EMAIL_CLAIM, query),
+                new BasicDBObject(FIELD_DISPLAY_NAME, query),
+                new BasicDBObject(FIELD_FIRST_NAME, query),
+                new BasicDBObject(FIELD_LAST_NAME, query));
+
         // if query contains wildcard, use the regex query
         if (query.contains("*")) {
             String compactQuery = query.replaceAll("\\*+", ".*");
             String regex = "^" + compactQuery;
-            searchQuery = new BasicDBObject(FIELD_USERNAME, Pattern.compile(regex, Pattern.CASE_INSENSITIVE));
+            Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
+            searchQuery = or(
+                    new BasicDBObject(FIELD_USERNAME, pattern),
+                    new BasicDBObject(FIELD_EMAIL, pattern),
+                    new BasicDBObject(FIELD_EMAIL_CLAIM, pattern),
+                    new BasicDBObject(FIELD_DISPLAY_NAME, pattern),
+                    new BasicDBObject(FIELD_FIRST_NAME, pattern),
+                    new BasicDBObject(FIELD_LAST_NAME, pattern));
         }
 
         Bson mongoQuery = and(
@@ -119,9 +145,23 @@ public class MongoUserRepository extends AbstractManagementMongoRepository imple
     }
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int page, int size) {
+    public Single<Page<User>> search(ReferenceType referenceType, String referenceId, FilterCriteria criteria, int page, int size) {
+        try {
+            BasicDBObject searchQuery = BasicDBObject.parse(FilterCriteriaParser.parse(criteria));
+
+            Bson mongoQuery = and(
+                    eq(FIELD_REFERENCE_TYPE, referenceType.name()),
+                    eq(FIELD_REFERENCE_ID, referenceId),
+                    searchQuery);
+
+            Single<Long> countOperation = Observable.fromPublisher(usersCollection.countDocuments(mongoQuery)).first(0l);
+            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));
+        } catch (Exception ex) {
+            logger.error("An error has occurred while searching users with criteria {}", criteria, ex);
+            return Single.error(new TechnicalException("An error has occurred while searching users with filter criteria", ex));
+        }
 
-        return search(DOMAIN, domain, query, page, size);
     }
 
     @Override
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 6b673b7596..2344126709 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
@@ -19,6 +19,7 @@ import io.gravitee.am.model.User;
 import io.gravitee.am.model.analytics.AnalyticsQuery;
 import io.gravitee.am.model.common.Page;
 import io.gravitee.am.model.ReferenceType;
+import io.gravitee.am.repository.management.api.search.FilterCriteria;
 import io.gravitee.am.service.model.NewUser;
 import io.gravitee.am.service.model.UpdateUser;
 import io.reactivex.Completable;
@@ -44,7 +45,7 @@ public interface UserService {
 
     Single<Page<User>> search(ReferenceType referenceType, String referenceId, String query, int page, int size);
 
-    Single<Page<User>> search(String domain, String query, int page, int size);
+    Single<Page<User>> search(ReferenceType referenceType, String referenceId, FilterCriteria filterCriteria, 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 f36787cc37..3146131e6b 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
@@ -26,8 +26,12 @@ import io.gravitee.am.model.common.Page;
 import io.gravitee.am.model.common.event.Event;
 import io.gravitee.am.model.common.event.Payload;
 import io.gravitee.am.repository.management.api.UserRepository;
+import io.gravitee.am.repository.management.api.search.FilterCriteria;
 import io.gravitee.am.service.*;
-import io.gravitee.am.service.exception.*;
+import io.gravitee.am.service.exception.AbstractManagementException;
+import io.gravitee.am.service.exception.TechnicalManagementException;
+import io.gravitee.am.service.exception.UserAlreadyExistsException;
+import io.gravitee.am.service.exception.UserNotFoundException;
 import io.gravitee.am.service.model.NewUser;
 import io.gravitee.am.service.model.UpdateUser;
 import io.gravitee.am.service.validators.UserValidator;
@@ -105,8 +109,13 @@ public class UserServiceImpl implements UserService {
     }
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int page, int size) {
-        return search(ReferenceType.DOMAIN, domain, query, page, size);
+    public Single<Page<User>> search(ReferenceType referenceType, String referenceId, FilterCriteria filterCriteria, int page, int size) {
+        LOGGER.debug("Search users for {} {} with filter {}", referenceType, referenceId, filterCriteria);
+        return userRepository.search(referenceType, referenceId, filterCriteria, page, size)
+                .onErrorResumeNext(ex -> {
+                    LOGGER.error("An error occurs while trying to search users for {} {} and filter {}", referenceType, referenceId, filterCriteria, ex);
+                    return Single.error(new TechnicalManagementException(String.format("An error occurs while trying to find users for %s %s and filter %s", referenceType, referenceId, filterCriteria), ex));
+                });
     }
 
     @Override
diff --git a/gravitee-am-ui/src/app/app.module.ts b/gravitee-am-ui/src/app/app.module.ts
index 50abb8a558..5f5e3b3c30 100644
--- a/gravitee-am-ui/src/app/app.module.ts
+++ b/gravitee-am-ui/src/app/app.module.ts
@@ -134,7 +134,7 @@ import { SettingsComponent } from './settings/settings.component';
 import { HumanDatePipe } from './pipes/human-date.pipe';
 import { MapToIterablePipe } from './pipes/map-to-iterable.pipe';
 import { DummyComponent } from './components/dummy/dummy.component';
-import { UsersComponent } from './domain/settings/users/users.component';
+import {UsersComponent, UsersSearchInfoDialog} from './domain/settings/users/users.component';
 import { UserComponent } from './domain/settings/users/user/user.component';
 import { UserCreationComponent } from './domain/settings/users/creation/user-creation.component';
 import { UserClaimComponent } from './domain/settings/users/creation/user-claim.component';
@@ -456,7 +456,8 @@ import { ApplicationLoginSettingsComponent } from './domain/applications/applica
     UserAvatarComponent,
     NotFoundComponent,
     UmaComponent,
-    LoginSettingsComponent
+    LoginSettingsComponent,
+    UsersSearchInfoDialog
   ],
   imports: [
     BrowserModule,
@@ -571,7 +572,8 @@ import { ApplicationLoginSettingsComponent } from './domain/applications/applica
     AddUserRolesComponent,
     AddGroupRolesComponent,
     DomainMembershipsDialog,
-    ApplicationMembershipsDialog
+    ApplicationMembershipsDialog,
+    UsersSearchInfoDialog
   ],
   bootstrap: [AppComponent]
 })
diff --git a/gravitee-am-ui/src/app/domain/applications/application/endpoints/endpoints.component.html b/gravitee-am-ui/src/app/domain/applications/application/endpoints/endpoints.component.html
index 82048dd9b1..85d5ae85a1 100644
--- a/gravitee-am-ui/src/app/domain/applications/application/endpoints/endpoints.component.html
+++ b/gravitee-am-ui/src/app/domain/applications/application/endpoints/endpoints.component.html
@@ -70,7 +70,7 @@
         <mat-hint>Exposes the JWKs (JSON Web Keys) used to sign or encrypt JWTs issued by the authorization server</mat-hint>
         <button mat-icon-button matSuffix matTooltip="Copy to clipboard" ngxClipboard [cbContent]="endpoint('/oidc/.well-known/jwks.json')" (cbOnSuccess)="valueCopied('Copied to clipboard')" (click)="$event.preventDefault();"><mat-icon>content_copy</mat-icon></button>
       </mat-form-field>
-      <mat-form-field appearance="outline" floatLabel="always">
+      <mat-form-field *ngIf="scimEnabled()" appearance="outline" floatLabel="always">
         <mat-label>SCIM Configuration endpoint</mat-label>
         <input matInput type="text" name="scimConfigurationEndpoint" [value]="endpoint('/scim/ServiceProviderConfig')" disabled>
         <mat-hint>The SCIM configuration endpoint allows third party systems to get the complete SCIM configuration</mat-hint>
diff --git a/gravitee-am-ui/src/app/domain/applications/application/endpoints/endpoints.component.ts b/gravitee-am-ui/src/app/domain/applications/application/endpoints/endpoints.component.ts
index 69dbe7fd2e..c653eaf09e 100644
--- a/gravitee-am-ui/src/app/domain/applications/application/endpoints/endpoints.component.ts
+++ b/gravitee-am-ui/src/app/domain/applications/application/endpoints/endpoints.component.ts
@@ -54,4 +54,8 @@ export class ApplicationEndpointsComponent implements OnInit {
   uma2Enabled() {
     return this.domain.uma && this.domain.uma.enabled;
   }
+
+  scimEnabled() {
+    return this.domain.scim && this.domain.scim.enabled;
+  }
 }
diff --git a/gravitee-am-ui/src/app/domain/settings/users/dialog/users-search-info.component.html b/gravitee-am-ui/src/app/domain/settings/users/dialog/users-search-info.component.html
new file mode 100644
index 0000000000..237caf8ce4
--- /dev/null
+++ b/gravitee-am-ui/src/app/domain/settings/users/dialog/users-search-info.component.html
@@ -0,0 +1,123 @@
+<!--
+
+    Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<div>
+  <h1 matDialogTitle>Search Users</h1>
+  <mat-dialog-content>
+    <h5>Information</h5>
+    <mat-divider></mat-divider>
+    <small>
+      When searching for users, you can create queries using SCIM 2.0 query syntax to refine your search (advanced search option).
+      The search query must contain at least one valid expression with an attribute name followed by an attribute operator and an optional value.
+      Multiple expressions may be combined using the logical operators ("and" / "or"). Operators "[", "]" and "not" are not supported.
+    </small>
+
+    <h5>Operators</h5>
+    <mat-divider></mat-divider>
+    <small>The following operators are supported :</small>
+    <pre>
+      <![CDATA[
+       +----------+-------------+------------------------------------------+
+       | Operator | Description | Behavior                                 |
+       +----------+-------------+------------------------------------------+
+       | eq       | equal       | The attribute and operator values must   |
+       |          |             | be identical for a match.                |
+       |          |             |                                          |
+       | ne       | not equal   | The attribute and operator values are    |
+       |          |             | not identical.                           |
+       |          |             |                                          |
+       | co       | contains    | The entire operator value must be a      |
+       |          |             | substring of the attribute value for a   |
+       |          |             | match.                                   |
+       |          |             |                                          |
+       | sw       | starts with | The entire operator value must be a      |
+       |          |             | substring of the attribute value,        |
+       |          |             | starting at the beginning of the         |
+       |          |             | attribute value.  This criterion is      |
+       |          |             | satisfied if the two strings are         |
+       |          |             | identical.                               |
+       |          |             |                                          |
+       | ew       | ends with   | The entire operator value must be a      |
+       |          |             | substring of the attribute value,        |
+       |          |             | matching at the end of the attribute     |
+       |          |             | value.  This criterion is satisfied if   |
+       |          |             | the two strings are identical.           |
+       |          |             |                                          |
+       | pr       | present     | If the attribute has a non-empty or      |
+       |          | (has value) | non-null value, or if it contains a      |
+       |          |             | non-empty node for complex attributes,   |
+       |          |             | there is a match.                        |
+       |          |             |                                          |
+       | gt       | greater     | If the attribute value is greater than   |
+       |          | than        | the operator value, there is a match.    |
+       |          |             | The actual comparison is dependent on    |
+       |          |             | the attribute type.  For string          |
+       |          |             | attribute types, this is a               |
+       |          |             | lexicographical comparison, and for      |
+       |          |             | DateTime types, it is a chronological    |
+       |          |             | comparison.                              |
+       |          |             |                                          |
+       | ge       | greater     | If the attribute value is greater than   |
+       |          | than or     | or equal to the operator value, there is |
+       |          | equal to    | a match.  The actual comparison is       |
+       |          |             | dependent on the attribute type.  For    |
+       |          |             | string attribute types, this is a        |
+       |          |             | lexicographical comparison, and for      |
+       |          |             | DateTime types, it is a chronological    |
+       |          |             | comparison.                              |
+       |          |             |                                          |
+       | lt       | less than   | If the attribute value is less than the  |
+       |          |             | operator value, there is a match.  The   |
+       |          |             | actual comparison is dependent on the    |
+       |          |             | attribute type.  For string attribute    |
+       |          |             | types, this is a lexicographical         |
+       |          |             | comparison, and for DateTime types, it   |
+       |          |             | is a chronological comparison.           |
+       |          |             |                                          |
+       | le       | less than   | If the attribute value is less than or   |
+       |          | or equal to | equal to the operator value, there is a  |
+       |          |             | match.  The actual comparison is         |
+       |          |             | dependent on the attribute type.  For    |
+       |          |             | string attribute types, this is a        |
+       |          |             | lexicographical comparison, and for      |
+       |          |             | DateTime types, it is a chronological    |
+       |          |             | comparison.                              |
+       +----------+-------------+------------------------------------------+
+      ]]>
+    </pre>
+
+    <h5>Examples</h5>
+    <mat-divider></mat-divider>
+    <small>The following examples will help you to start with this feature. All the attribute names are based on the User model provided by the Rest API.</small>
+    <pre>
+      <![CDATA[
+      List disabled users
+      -> enabled eq false
+
+      List users updated after 06/01/2019 but before 01/01/2020
+      -> updatedAt gt "2019-06-01T00:00:00.000Z" and updatedAt lt "2020-01-01T00:00:00.000Z"
+
+      List users by first name
+      -> firstName co "john"
+      ]]>
+    </pre>
+  </mat-dialog-content>
+  <mat-dialog-actions>
+    <span fxFlex></span>
+    <button type="button" mat-raised-button (click)="dialogRef.close()" class="dialog-cancel">Close</button>
+  </mat-dialog-actions>
+</div>
diff --git a/gravitee-am-ui/src/app/domain/settings/users/users.component.html b/gravitee-am-ui/src/app/domain/settings/users/users.component.html
index a6522141d2..1eb085e971 100644
--- a/gravitee-am-ui/src/app/domain/settings/users/users.component.html
+++ b/gravitee-am-ui/src/app/domain/settings/users/users.component.html
@@ -18,11 +18,20 @@
 <div class="gv-page-container" fxLayout="column">
   <h1 *ngIf="!isEmpty">Users</h1>
 
-  <div *ngIf="!isEmpty" class="users-search">
-    <mat-form-field>
-      <mat-icon matPrefix="">search</mat-icon>
-      <input type="text" matInput placeholder="Search" (input)="onSearch($event)">
-      <mat-hint align="end">By username</mat-hint>
+  <div *ngIf="!isEmpty" class="users-search" fxLayout="row" style="align-items: baseline;">
+    <mat-form-field id="users-search-input" appearance="fill" floatLabel="always" fxFlex="80">
+      <mat-icon matPrefix>search</mat-icon>
+      <mat-label>Search for users</mat-label>
+      <input [(ngModel)]="searchValue" type="text" matInput placeholder="Search" (keyup.enter)="onSearch()">
+      <button *ngIf="searchValue" mat-icon-button matSuffix (click)="discardSearchValue()"><mat-icon>close</mat-icon></button>
+      <mat-hint align="end">Press <b>enter</b> to perform the search request <mat-icon (click)="openDialog()" style="font-size: 15px; color:black; cursor: pointer;">info_outline</mat-icon></mat-hint>
+    </mat-form-field>
+    <mat-form-field id="users-search-mode" appearance="fill" floatLabel="always" fxFlex="20">
+      <mat-label>Search by</mat-label>
+      <mat-select [(value)]="searchMode">
+        <mat-option value="standard">Standard</mat-option>
+        <mat-option value="advanced">Advanced (SCIM 2.0 syntax)</mat-option>
+      </mat-select>
     </mat-form-field>
   </div>
 
diff --git a/gravitee-am-ui/src/app/domain/settings/users/users.component.scss b/gravitee-am-ui/src/app/domain/settings/users/users.component.scss
index da5ef5a4e3..c4d5ab1c9f 100644
--- a/gravitee-am-ui/src/app/domain/settings/users/users.component.scss
+++ b/gravitee-am-ui/src/app/domain/settings/users/users.component.scss
@@ -1,4 +1,5 @@
 ngx-datatable {
+  margin-top: 10px;
   .user-header {
     margin: 5px;
     align-items: center;
@@ -20,16 +21,9 @@ ngx-datatable {
 }
 
 .users-search {
-  text-align: center;
-
-  mat-form-field {
-    width: 70%;
-  }
-
-  mat-icon {
-    vertical-align: middle;
-    padding-bottom: 4px;
-  }
+  font-size: 14px;
+  margin: 0 auto;
+  width: 80%;
 }
 
 :host /deep/ .user-name-cell {
@@ -67,3 +61,12 @@ ngx-datatable {
   color: white;
   background: red;
 }
+
+:host /deep/ #users-search-input.mat-form-field-appearance-fill .mat-form-field-flex {
+  border-radius: 4px 0;
+}
+
+:host /deep/ #users-search-mode.mat-form-field-appearance-fill .mat-form-field-flex {
+  border-radius: 0px 4px 0px 0px;
+  background: #a9a9a9;
+}
diff --git a/gravitee-am-ui/src/app/domain/settings/users/users.component.ts b/gravitee-am-ui/src/app/domain/settings/users/users.component.ts
index 9898cc01cc..154b5aaa51 100644
--- a/gravitee-am-ui/src/app/domain/settings/users/users.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/users/users.component.ts
@@ -14,12 +14,13 @@
  * limitations under the License.
  */
 import { Component, OnInit } from '@angular/core';
-import { UserService} from "../../../services/user.service";
-import { SnackbarService } from "../../../services/snackbar.service";
-import { DialogService } from "../../../services/dialog.service";
-import { ActivatedRoute, Router } from "@angular/router";
-import { OrganizationService } from "../../../services/organization.service";
-import {AuthService} from "../../../services/auth.service";
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
+import { ActivatedRoute, Router } from '@angular/router';
+import { UserService} from '../../../services/user.service';
+import { SnackbarService } from '../../../services/snackbar.service';
+import { DialogService } from '../../../services/dialog.service';
+import { OrganizationService } from '../../../services/organization.service';
+import { AuthService } from '../../../services/auth.service';
 
 @Component({
   selector: 'app-users',
@@ -28,6 +29,8 @@ import {AuthService} from "../../../services/auth.service";
 })
 export class UsersComponent implements OnInit {
   private searchValue: string;
+  private isLoading: boolean;
+  private hasValue: boolean;
   organizationContext: boolean;
   requiredReadPermission: string;
   pagedUsers: any;
@@ -35,6 +38,7 @@ export class UsersComponent implements OnInit {
   domainId: string;
   page: any = {};
   createMode: boolean;
+  searchMode = 'standard';
 
   constructor(private userService: UserService,
               private organizationService: OrganizationService,
@@ -42,7 +46,8 @@ export class UsersComponent implements OnInit {
               private snackbarService: SnackbarService,
               private authService: AuthService,
               private route: ActivatedRoute,
-              private router: Router) {
+              private router: Router,
+              public dialog: MatDialog) {
     this.page.pageNumber = 0;
     this.page.size = 25;
   }
@@ -60,18 +65,26 @@ export class UsersComponent implements OnInit {
     this.pagedUsers = this.route.snapshot.data['users'];
     this.users = this.pagedUsers.data;
     this.page.totalElements = this.pagedUsers.totalCount;
+    this.hasValue = this.pagedUsers.totalCount > 0;
   }
 
   get isEmpty() {
-    return !this.users || this.users.length === 0 && !this.searchValue;
+    return !this.users || this.users.length === 0 && (!this.searchValue && !this.hasValue) && !this.isLoading;
   }
 
   loadUsers() {
-    let findUsers = (this.searchValue) ?
-      this.userService.search(this.domainId, this.searchValue + '*', this.page.pageNumber, this.page.size, this.organizationContext) :
-      (this.organizationContext ? this.organizationService.users(this.page.pageNumber, this.page.size) : this.userService.findByDomain(this.domainId, this.page.pageNumber, this.page.size));
-
+    let findUsers;
+    if (this.searchValue) {
+      const searchTerm = this.searchMode === 'standard' ? 'q=' + this.searchValue + '*' : 'filter=' + this.searchValue;
+      findUsers = this.userService.search(this.domainId, searchTerm, this.page.pageNumber, this.page.size, this.organizationContext);
+    } else {
+      findUsers = this.organizationContext
+        ? this.organizationService.users(this.page.pageNumber, this.page.size)
+        : this.userService.findByDomain(this.domainId, this.page.pageNumber, this.page.size);
+    }
+    this.isLoading = true;
     findUsers.subscribe(pagedUsers => {
+      this.isLoading = false;
       this.page.totalElements = pagedUsers.totalCount;
       this.users = pagedUsers.data;
     });
@@ -84,7 +97,7 @@ export class UsersComponent implements OnInit {
       .subscribe(res => {
         if (res) {
           this.userService.delete(this.domainId, id).subscribe(response => {
-            this.snackbarService.open("User deleted");
+            this.snackbarService.open('User deleted');
             this.page.pageNumber = 0;
             this.loadUsers();
           });
@@ -92,8 +105,12 @@ export class UsersComponent implements OnInit {
       });
   }
 
-  onSearch(event) {
-    this.searchValue = event.target.value;
+  discardSearchValue() {
+    this.searchValue = null;
+    this.loadUsers();
+  }
+
+  onSearch() {
     this.loadUsers();
   }
 
@@ -109,4 +126,17 @@ export class UsersComponent implements OnInit {
   hasReadPermissions(): boolean {
     return this.authService.hasPermissions([this.requiredReadPermission]);
   }
+
+  openDialog() {
+    this.dialog.open(UsersSearchInfoDialog, {});
+  }
+}
+
+@Component({
+  selector: 'users-search-info-dialog',
+  templateUrl: './dialog/users-search-info.component.html',
+})
+export class UsersSearchInfoDialog {
+  constructor(public dialogRef: MatDialogRef<UsersSearchInfoDialog>) {}
 }
+
diff --git a/gravitee-am-ui/src/app/services/organization.service.ts b/gravitee-am-ui/src/app/services/organization.service.ts
index bf1945821b..38b1946ead 100644
--- a/gravitee-am-ui/src/app/services/organization.service.ts
+++ b/gravitee-am-ui/src/app/services/organization.service.ts
@@ -110,7 +110,7 @@ export class OrganizationService {
   }
 
   searchUsers(searchTerm, page, size): Observable<any> {
-    return this.http.get<any>(this.organizationURL + '/users?q=' + searchTerm + '&page=' + page + '&size=' + size);
+    return this.http.get<any>(this.organizationURL + '/users?' + searchTerm + '&page=' + page + '&size=' + size);
   }
 
   roles(type?): Observable<any> {
diff --git a/gravitee-am-ui/src/app/services/user.service.ts b/gravitee-am-ui/src/app/services/user.service.ts
index ec2233a2f3..089d5091b9 100644
--- a/gravitee-am-ui/src/app/services/user.service.ts
+++ b/gravitee-am-ui/src/app/services/user.service.ts
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 import {Injectable} from '@angular/core';
-import {AppConfig} from "../../config/app.config";
-import {HttpClient} from "@angular/common/http";
-import {Observable} from "rxjs";
-import {OrganizationService} from "./organization.service";
+import {AppConfig} from '../../config/app.config';
+import {HttpClient} from '@angular/common/http';
+import {Observable} from 'rxjs';
+import {OrganizationService} from './organization.service';
 
 @Injectable()
 export class UserService {
@@ -27,19 +27,19 @@ export class UserService {
               private organizationService: OrganizationService) { }
 
   findByDomain(domainId, page, size): Observable<any> {
-    return this.http.get<any>(this.usersURL + domainId + "/users?page=" + page + "&size=" + size);
+    return this.http.get<any>(this.usersURL + domainId + '/users?page=' + page + '&size=' + size);
   }
 
   get(domainId, id): Observable<any> {
-    return this.http.get<any>(this.usersURL + domainId + "/users/" + id);
+    return this.http.get<any>(this.usersURL + domainId + '/users/' + id);
   }
 
   create(domainId, user): Observable<any> {
-    return this.http.post<any>(this.usersURL + domainId + "/users", user);
+    return this.http.post<any>(this.usersURL + domainId + '/users', user);
   }
 
   update(domainId, id, user): Observable<any> {
-    return this.http.put<any>(this.usersURL + domainId + "/users/" + id, {
+    return this.http.put<any>(this.usersURL + domainId + '/users/' + id, {
       'firstName' : user.firstName,
       'lastName' : user.lastName,
       'email' : user.email,
@@ -50,64 +50,64 @@ export class UserService {
   }
 
   updateStatus(domainId, id, status): Observable<any> {
-    return this.http.put<any>(this.usersURL + domainId + "/users/" + id + '/status', {
+    return this.http.put<any>(this.usersURL + domainId + '/users/' + id + '/status', {
       'enabled' : status
     });
   }
 
   delete(domainId, id): Observable<any> {
-    return this.http.delete<any>(this.usersURL + domainId + "/users/" + id);
+    return this.http.delete<any>(this.usersURL + domainId + '/users/' + id);
   }
 
   resendRegistrationConfirmation(domainId, id): Observable<any> {
-    return this.http.post<any>(this.usersURL + domainId + "/users/" + id + "/sendRegistrationConfirmation", {});
+    return this.http.post<any>(this.usersURL + domainId + '/users/' + id + '/sendRegistrationConfirmation', {});
   }
 
   resetPassword(domainId, id, password): Observable<any> {
-    return this.http.post<any>(this.usersURL + domainId + "/users/" + id + "/resetPassword", {
+    return this.http.post<any>(this.usersURL + domainId + '/users/' + id + '/resetPassword', {
       'password': password
     });
   }
 
   unlock(domainId, id): Observable<any> {
-    return this.http.post<any>(this.usersURL + domainId + "/users/" + id + "/unlock", {});
+    return this.http.post<any>(this.usersURL + domainId + '/users/' + id + '/unlock', {});
   }
 
   search(domainId, searchTerm, page, size, organizationContext): Observable<any> {
     if (organizationContext) {
       return this.organizationService.searchUsers(searchTerm, page, size);
     }
-    return this.http.get<any>(this.usersURL + domainId + "/users?q=" + searchTerm + "&page=" + page + "&size=" + size);
+    return this.http.get<any>(this.usersURL + domainId + '/users?' + searchTerm + '&page=' + page + '&size=' + size);
   }
 
   consents(domainId, userId, clientId): Observable<any> {
-    return this.http.get<any>(this.usersURL + domainId + "/users/" + userId + "/consents" + (clientId ? "?clientId=" + clientId : ""));
+    return this.http.get<any>(this.usersURL + domainId + '/users/' + userId + '/consents' + (clientId ? '?clientId=' + clientId : ''));
   }
 
   revokeConsents(domainId, userId, clientId): Observable<any> {
-    return this.http.delete<any>(this.usersURL + domainId + "/users/" + userId + "/consents" + (clientId ? "?clientId=" + clientId : ""));
+    return this.http.delete<any>(this.usersURL + domainId + '/users/' + userId + '/consents' + (clientId ? '?clientId=' + clientId : ''));
   }
 
   revokeConsent(domainId, userId, consentId): Observable<any> {
-    return this.http.delete<any>(this.usersURL + domainId + "/users/" + userId + "/consents/" + consentId);
+    return this.http.delete<any>(this.usersURL + domainId + '/users/' + userId + '/consents/' + consentId);
   }
 
   roles(domainId, userId): Observable<any> {
-    return this.http.get<any>(this.usersURL + domainId + "/users/" + userId + "/roles");
+    return this.http.get<any>(this.usersURL + domainId + '/users/' + userId + '/roles');
   }
 
   revokeRole(domainId, userId, roleId, organizationContext): Observable<any> {
     if (organizationContext) {
       return this.organizationService.revokeUserRole(userId, roleId);
     }
-    return this.http.delete<any>(this.usersURL + domainId + "/users/" + userId + "/roles/" + roleId);
+    return this.http.delete<any>(this.usersURL + domainId + '/users/' + userId + '/roles/' + roleId);
   }
 
   assignRoles(domainId, userId, roles, organizationContext): Observable<any> {
     if (organizationContext) {
       return this.organizationService.assignUserRoles(userId, roles);
     }
-    return this.http.post<any>(this.usersURL + domainId + "/users/" + userId + "/roles", roles);
+    return this.http.post<any>(this.usersURL + domainId + '/users/' + userId + '/roles', roles);
   }
 
   factors(domainId, userId): Observable<any> {
diff --git a/postman/collections/graviteeio-am-scim-collection-app-version.json b/postman/collections/graviteeio-am-scim-collection-app-version.json
index 3b9a2e04c5..8acc6f2869 100644
--- a/postman/collections/graviteeio-am-scim-collection-app-version.json
+++ b/postman/collections/graviteeio-am-scim-collection-app-version.json
@@ -1,6 +1,6 @@
 {
 	"info": {
-		"_postman_id": "fd3b98ad-1ada-4d9f-a538-b545a7fc9971",
+		"_postman_id": "1e8784b9-4e71-4f44-972d-348196dfd13a",
 		"name": "Gravitee.io - AM - SCIM - app version",
 		"description": "Test System for Cross-domain Identity Management\nSCIM 2.0 is released as RFC7642, RFC7643 and RFC7644",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
@@ -15,7 +15,7 @@
 						{
 							"listen": "test",
 							"script": {
-								"id": "cd9a60b8-e05a-432a-8686-6e915265011a",
+								"id": "702efad1-64e7-473e-abb4-9116327fc2d4",
 								"exec": [
 									"",
 									"pm.test(\"Status code is 200\", function () {",
@@ -77,7 +77,7 @@
 						{
 							"listen": "test",
 							"script": {
-								"id": "38e1a86b-e260-4b9a-96c4-7985b7ce5461",
+								"id": "bf2325a8-e22d-4006-82dc-26bea43ec6f6",
 								"exec": [
 									"pm.test(\"Status code is 201\", function () {",
 									"    pm.response.to.have.status(201);",
@@ -130,7 +130,7 @@
 						{
 							"listen": "test",
 							"script": {
-								"id": "01df0737-a8dc-407d-96ad-7b8f646cde42",
+								"id": "776be139-dc72-4303-9bc7-08f41160430d",
 								"exec": [
 									"// wait for sync process",
 									"setTimeout(function(){}, 10000);"
@@ -179,7 +179,7 @@
 						{
 							"listen": "test",
 							"script": {
-								"id": "0a51c5d5-6aa9-46a6-9e02-dce04f51d1cc",
+								"id": "057decc6-d12b-41d2-ad6a-b31f4d2c7ea3",
 								"exec": [
 									"pm.test(\"Status code is 200\", function () {",
 									"    pm.response.to.have.status(200);",
@@ -230,7 +230,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "3e283282-6165-471d-85a3-fa95b40438c3",
+										"id": "eb7dec3b-3645-4a24-8399-c18b4b8eb278",
 										"exec": [
 											"pm.test(\"Status code is 404\", function () {",
 											"    pm.response.to.have.status(404);",
@@ -263,7 +263,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "084a5dd9-ed9b-4938-9d50-adc45f4f309f",
+										"id": "ebaf443d-cde2-4f94-bb57-90dd68930ae7",
 										"exec": [
 											"pm.test(\"Status code is 404\", function () {",
 											"    pm.response.to.have.status(404);",
@@ -296,7 +296,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "d15c97e7-c031-4f3b-9d86-0fe44a582d5b",
+										"id": "d5b7265e-7e12-4f36-8605-659f3b5881cd",
 										"exec": [
 											"pm.test(\"Status code is 404\", function () {",
 											"    pm.response.to.have.status(404);",
@@ -336,7 +336,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "99702a25-2695-41c2-99cc-59c4de631a21",
+										"id": "6a01c163-ae6a-462f-8fc2-deb2a191c218",
 										"exec": [
 											"pm.test(\"Status code is 200\", function () {",
 											"    pm.response.to.have.status(200);",
@@ -404,7 +404,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "55087029-be20-4c12-9f22-daae96d94cfb",
+										"id": "a3cb60ee-c3bb-428c-89d8-fe8ca8676c63",
 										"exec": [
 											"pm.test(\"Status code is 200\", function () {",
 											"    pm.response.to.have.status(200);",
@@ -415,7 +415,7 @@
 											"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig']);",
 											"    pm.expect(jsonData.patch.supported).to.eql(false);",
 											"    pm.expect(jsonData.bulk.supported).to.eql(false);",
-											"    pm.expect(jsonData.filter.supported).to.eql(false);",
+											"    pm.expect(jsonData.filter.supported).to.eql(true);",
 											"    pm.expect(jsonData.changePassword.supported).to.eql(false);",
 											"    pm.expect(jsonData.sort.supported).to.eql(false);",
 											"    pm.expect(jsonData.etag.supported).to.eql(false);",
@@ -456,7 +456,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "3d4d4d74-769a-4527-bf5c-59d90ba8a3e3",
+										"id": "9d72f159-7a4c-4086-8eb8-c0af50a6ac5f",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -496,7 +496,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "171f53b1-3d4e-4853-a074-2da4e353b076",
+										"id": "ab1176e6-a196-40d7-a73a-79822ee9b7e4",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -542,7 +542,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "cfd026b7-b900-4560-a0a9-bf69c29e050d",
+										"id": "e2dc1b9c-4ccd-4fc3-9d27-2024d488d8bd",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -586,7 +586,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "173eba8b-abd7-4064-9a54-f81694bda170",
+										"id": "b3084f67-2776-40eb-8abe-86289a982c2d",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -636,7 +636,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "563a8849-2048-403e-bc88-a0bed597a5c5",
+										"id": "aeec2068-5950-4aab-99fc-acca6ae5f04d",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -681,7 +681,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "d26f0480-4f1d-497b-bf0e-d2a7edd3d246",
+										"id": "e745e539-7ee6-4f0d-a3b4-20e1f6c3b1e9",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -732,7 +732,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "4fb173af-8ae3-4da3-bca2-70cce2575d43",
+										"id": "f42a6710-83d9-4b8f-9005-c8d89fffe45f",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -777,7 +777,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "8975679e-4eec-4f35-852f-030445ee5e3d",
+										"id": "fb9a71a1-e099-46f8-a5e4-51f041964f3b",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -828,7 +828,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "49534273-ef8c-4334-b484-015ea9a2110a",
+										"id": "bdc15aa0-2039-44eb-8921-dc7db89536f9",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -868,7 +868,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "e2dbe074-b36d-421d-b966-dc87eabce908",
+										"id": "ffca470d-4f1d-4d0d-b09a-11f7a523a23f",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -914,7 +914,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "9b03675d-5364-4b9a-9d6d-18239b200902",
+										"id": "5a45da68-f916-4c44-81a3-1ce3183ae50c",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -959,7 +959,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "cf2af698-eb0b-4380-bed2-bb6fdd4b46cf",
+										"id": "788b6789-2bb6-499b-9be3-a98797f0bd76",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -1010,7 +1010,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "302dddee-a124-454a-94d1-e8f47f2336fb",
+										"id": "cd2acf95-af43-47d7-9a3a-7c84bd95295a",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -1054,7 +1054,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "9e71a107-6555-4c6f-9dac-7ad5e89c1b51",
+										"id": "37a9e324-b370-4f5b-8084-708b16439fa0",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -1104,7 +1104,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "d342481a-6911-420e-86e6-b4ce98bc90e9",
+										"id": "7075a551-1b4d-491e-ae67-f1b36dfd67cd",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -1149,7 +1149,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "97318852-82c0-403c-bdd6-66c944cef716",
+										"id": "829aa230-7348-4dff-8440-2037124850e8",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -1207,7 +1207,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "d9b25901-249b-408f-bc50-737ee7f2d23b",
+										"id": "cd2c3ebc-b37e-45dc-bbcb-c5ab596b74c2",
 										"exec": [
 											"pm.test(\"Status code is 201\", function () {",
 											"    pm.response.to.have.status(201);",
@@ -1263,7 +1263,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "410e994b-ac12-40bc-b22a-0b9f8573fb28",
+										"id": "d601ea2b-3cbd-4401-9522-9cb2bb4ef419",
 										"exec": [
 											"pm.test(\"Status code is 200\", function () {",
 											"    pm.response.to.have.status(200);",
@@ -1318,7 +1318,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "2ff83756-5e78-4919-8594-38df1a3cfc85",
+										"id": "b4885851-5757-4109-ae7b-e0e8ad5ff34a",
 										"exec": [
 											"pm.test(\"Status code is 200\", function () {",
 											"    pm.response.to.have.status(200);",
@@ -1337,7 +1337,7 @@
 								{
 									"listen": "prerequest",
 									"script": {
-										"id": "8fb60b6b-b8df-4570-b51d-5663f1a1f4cd",
+										"id": "13d355fd-07ba-4249-a528-0833bf32d6d9",
 										"exec": [
 											""
 										],
@@ -1398,7 +1398,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "4e6dc44f-554b-4aa4-a5fe-bc240f1a9a6d",
+												"id": "fb71a0c9-0f86-4572-97ae-16f7a93e2465",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1449,7 +1449,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "70d57c9e-564f-48da-837e-ba4454683060",
+												"id": "76e824d0-c6e0-4be3-a68c-86cbfef6046b",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1506,7 +1506,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "ff8dd646-22ea-46dd-bd06-9e97a184c392",
+												"id": "bd5c98dd-707a-486c-9668-cb69192dd7e8",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1563,7 +1563,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "55c18792-38c7-4814-b5b3-00ceef0a5c07",
+												"id": "35fdbc31-454b-4734-b909-8ca150d5115d",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1620,7 +1620,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "79f12714-45f1-4137-854d-bc61198cf4c4",
+												"id": "173ed4b8-e7e6-44bf-8996-3fbfff285ff2",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1677,7 +1677,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "b967933f-9c41-4921-8884-5586d6ac03c2",
+												"id": "bdb2d98b-6833-4a7a-ad1f-fe28f3c5b0b5",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1734,7 +1734,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "373de481-5340-45db-9c97-fce086f24c80",
+												"id": "8ddbea90-72f1-4e2f-bacc-aa15a1a28674",
 												"exec": [
 													"pm.test(\"Status code is 201\", function () {",
 													"    pm.response.to.have.status(201);",
@@ -1790,7 +1790,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "6ccd3c92-d68a-4543-a302-cc3e2f4f3078",
+												"id": "964b2b95-6a73-4bf9-9348-abdfaa587322",
 												"exec": [
 													"pm.test(\"Status code is 409\", function () {",
 													"    pm.response.to.have.status(409);",
@@ -1854,7 +1854,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "a09375d7-c8ca-4e59-99f7-9d0fa59b86c6",
+												"id": "a51c35e8-c748-4a70-a914-e9bc04ab48bd",
 												"exec": [
 													"pm.test(\"Status code is 404\", function () {",
 													"    pm.response.to.have.status(404);",
@@ -1911,7 +1911,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "1d6465b9-076b-43e9-87cf-e378be6404c8",
+												"id": "85bfbf1b-b0f1-456e-b595-65289fef1671",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1963,7 +1963,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "7c1b8f08-bf6e-4997-933c-ec980551bec6",
+												"id": "584e9999-92f4-43b7-b7d5-33309a9aaa8e",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2027,7 +2027,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "e36de2a7-32c3-4a00-a364-d23fa6905da3",
+												"id": "bb757cde-314b-438d-a69e-75bbb80ec32e",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2069,6 +2069,327 @@
 										}
 									},
 									"response": []
+								},
+								{
+									"name": "Filter users - invalid filter",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "45f020ac-5759-4308-9e58-a3f0e1fa240a",
+												"exec": [
+													"pm.test(\"Status code is 400\", function () {",
+													"    pm.response.to.have.status(400);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - invalid filter\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:Error']);",
+													"    pm.expect(jsonData.scimType).to.eql('invalidSyntax');",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"value": "Bearer {{access_token}}",
+												"type": "text"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=invalid",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "invalid"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Filter users - user not found",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "3e663247-dbd2-4dcf-94f0-e01868132f27",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - unknown user\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:ListResponse']);",
+													"    pm.expect(jsonData.totalResults).to.eql(0);",
+													"    pm.expect(jsonData.startIndex).to.eql(1);",
+													"    pm.expect(jsonData.itemsPerPage).to.eql(0);",
+													"    pm.expect(jsonData.Resources.length).to.eql(0);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{access_token}}"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=userName eq \"invalid\"",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "userName eq \"invalid\""
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Filter users - user found - simple filter - eq operand",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "c86b4a12-606a-401a-af83-faa93ab8abad",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - simple filter - eq\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:ListResponse']);",
+													"    pm.expect(jsonData.totalResults).to.eql(1);",
+													"    pm.expect(jsonData.startIndex).to.eql(1);",
+													"    pm.expect(jsonData.itemsPerPage).to.eql(1);",
+													"    pm.expect(jsonData.Resources.length).to.eql(1);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{access_token}}"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=userName eq \"bjensen@example.com\"",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "userName eq \"bjensen@example.com\""
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Filter users - user found - simple filter - regex operand",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "83ae9087-dbd5-42dc-a144-40082963c43c",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - simple filter - regex\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:ListResponse']);",
+													"    pm.expect(jsonData.totalResults).to.eql(1);",
+													"    pm.expect(jsonData.startIndex).to.eql(1);",
+													"    pm.expect(jsonData.itemsPerPage).to.eql(1);",
+													"    pm.expect(jsonData.Resources.length).to.eql(1);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{access_token}}"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=userName co \"bjensen\"",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "userName co \"bjensen\""
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Filter users - user found - complex filter - or operand",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "5b0b750b-f6fe-49fa-8849-a515551b5663",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - complex filter - or\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:ListResponse']);",
+													"    pm.expect(jsonData.totalResults).to.eql(1);",
+													"    pm.expect(jsonData.startIndex).to.eql(1);",
+													"    pm.expect(jsonData.itemsPerPage).to.eql(1);",
+													"    pm.expect(jsonData.Resources.length).to.eql(1);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{access_token}}"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=userName eq \"invalid\" or name.givenName eq \"Barbara\"",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "userName eq \"invalid\" or name.givenName eq \"Barbara\""
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Filter users - user found - complex filter - and operand",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "cb833f93-04a7-489d-b7da-af88502f61bd",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - complex filter - and\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:ListResponse']);",
+													"    pm.expect(jsonData.totalResults).to.eql(1);",
+													"    pm.expect(jsonData.startIndex).to.eql(1);",
+													"    pm.expect(jsonData.itemsPerPage).to.eql(1);",
+													"    pm.expect(jsonData.Resources.length).to.eql(1);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{access_token}}"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=name.givenName eq \"Barbara\" and active eq false and meta.created ge \"2020-10-14T20:18:43.048Z\"",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "name.givenName eq \"Barbara\" and active eq false and meta.created ge \"2020-10-14T20:18:43.048Z\""
+												}
+											]
+										}
+									},
+									"response": []
 								}
 							],
 							"protocolProfileBehavior": {},
@@ -2083,7 +2404,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "49918fc3-664a-44bf-968d-bd59ce7cc83c",
+												"id": "4f4c7cd9-63a8-46fc-a4bf-f1f79948633c",
 												"exec": [
 													"pm.test(\"Status code is 404\", function () {",
 													"    pm.response.to.have.status(404);",
@@ -2134,7 +2455,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "c048475d-f61c-440a-91ab-417ff1bdfada",
+												"id": "d0d71d89-a4c4-447d-874c-02979fcc0f15",
 												"exec": [
 													"pm.test(\"Status code is 204\", function () {",
 													"    pm.response.to.have.status(204);",
@@ -2178,7 +2499,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "1aa96f17-d43e-41d6-bd1e-95da688cc699",
+												"id": "515ee4d5-3944-40e4-9a8b-48b6b078d9d2",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2240,7 +2561,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "499ea95b-426d-413a-902a-cd9f5105de3d",
+												"id": "65916c19-8421-4070-af52-c02073685453",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -2291,7 +2612,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "b07a412d-855b-4744-98b0-9ab565621de0",
+												"id": "26f5545a-26b6-4649-9376-cfbdbc148d5f",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -2348,7 +2669,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "f74ec697-854f-4014-9eed-0f9537df76b6",
+												"id": "9c5703df-9041-4669-b438-a8d76aa7ad0d",
 												"exec": [
 													"pm.test(\"Status code is 201\", function () {",
 													"    pm.response.to.have.status(201);",
@@ -2404,7 +2725,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "a0564c07-2c05-4731-ae1e-59ebaf04e4bb",
+												"id": "baa23664-e630-4bbf-90a4-f682c899b464",
 												"exec": [
 													"pm.test(\"Status code is 409\", function () {",
 													"    pm.response.to.have.status(409);",
@@ -2468,7 +2789,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "1f9c7cd4-9934-4261-9ead-065df1e6a1a6",
+												"id": "3c2c6942-2917-46f6-93fe-d11d24b48228",
 												"exec": [
 													"pm.test(\"Status code is 404\", function () {",
 													"    pm.response.to.have.status(404);",
@@ -2525,7 +2846,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "7c3fa9d3-c21a-446b-b60a-c51503d7fd69",
+												"id": "e80c87d3-9448-4540-83a3-00f096bd57c2",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -2577,7 +2898,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "6abcce16-9627-47c2-8399-01dce19d7ef1",
+												"id": "4dc79ae1-e9e4-422e-ad46-2a3d60223a8d",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2641,7 +2962,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "959dbae9-c5d4-4b1c-b612-73f3f85bf236",
+												"id": "7d54ab43-fa08-4011-9161-52e377ff08e8",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2696,7 +3017,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "04cfa6c2-a703-48a9-878a-b2270232d341",
+												"id": "9122a083-8a10-4c65-b2c8-4881c76c91f1",
 												"exec": [
 													"pm.test(\"Status code is 404\", function () {",
 													"    pm.response.to.have.status(404);",
@@ -2747,7 +3068,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "0ddc4c29-672d-4f2c-80c2-38179b075dbe",
+												"id": "b53a1699-fe5f-4380-be29-796d298165e9",
 												"exec": [
 													"pm.test(\"Status code is 204\", function () {",
 													"    pm.response.to.have.status(204);",
@@ -2791,7 +3112,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "a5aa8ea8-b71a-48c1-91d3-7807d2920321",
+												"id": "6d38066c-1e79-4572-87f1-46556823ce7a",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2850,7 +3171,7 @@
 				{
 					"listen": "test",
 					"script": {
-						"id": "bc2eb12d-f2ba-451b-b148-64109fc1a315",
+						"id": "d797ca1f-8117-4437-818a-469adec6b0a8",
 						"exec": [
 							"pm.test(\"Status code is 204\", function () {",
 							"    pm.response.to.have.status(204);",
diff --git a/postman/collections/graviteeio-am-scim-collection.json b/postman/collections/graviteeio-am-scim-collection.json
index caa4df3a4a..71f6634ca4 100644
--- a/postman/collections/graviteeio-am-scim-collection.json
+++ b/postman/collections/graviteeio-am-scim-collection.json
@@ -1,6 +1,6 @@
 {
 	"info": {
-		"_postman_id": "adc51449-c8a9-4466-9391-b28f160010c5",
+		"_postman_id": "93b8a7d7-d3b6-4868-bf6f-3485bd26b49e",
 		"name": "Gravitee.io - AM - SCIM",
 		"description": "Test System for Cross-domain Identity Management\nSCIM 2.0 is released as RFC7642, RFC7643 and RFC7644",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
@@ -15,7 +15,7 @@
 						{
 							"listen": "test",
 							"script": {
-								"id": "5dfcf876-f7b9-4bd5-a613-5081bdcf48c8",
+								"id": "8f58b376-fb48-40fa-be00-c2eba9e4a2f4",
 								"exec": [
 									"",
 									"pm.test(\"Status code is 200\", function () {",
@@ -77,7 +77,7 @@
 						{
 							"listen": "test",
 							"script": {
-								"id": "fc5c0d6d-4396-4584-a4ec-b94555309588",
+								"id": "4c5b6f31-228b-4eeb-927a-7e6ff5006629",
 								"exec": [
 									"pm.test(\"Status code is 201\", function () {",
 									"    pm.response.to.have.status(201);",
@@ -130,7 +130,7 @@
 						{
 							"listen": "test",
 							"script": {
-								"id": "74b91973-e50c-45b8-bcc7-cd67494596f0",
+								"id": "f97bdacb-1b10-4d35-a2a2-526f498e55bb",
 								"exec": [
 									"// wait for sync process",
 									"setTimeout(function(){}, 10000);"
@@ -179,7 +179,7 @@
 						{
 							"listen": "test",
 							"script": {
-								"id": "daa4ea5b-b7e6-4d1b-a915-50e6c5fa1e51",
+								"id": "57580633-d7d9-46e1-9f47-185530f4883f",
 								"exec": [
 									"pm.test(\"Status code is 200\", function () {",
 									"    pm.response.to.have.status(200);",
@@ -230,7 +230,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "c53f8498-2f23-4adb-bbc6-69f41f5aea15",
+										"id": "f670da30-63c5-444e-bdf1-9dffc11d6ad7",
 										"exec": [
 											"pm.test(\"Status code is 404\", function () {",
 											"    pm.response.to.have.status(404);",
@@ -263,7 +263,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "a8f7c944-dcfc-4d9d-ae33-50190d95afbe",
+										"id": "97fce838-9013-4891-acbd-49ce98684da5",
 										"exec": [
 											"pm.test(\"Status code is 404\", function () {",
 											"    pm.response.to.have.status(404);",
@@ -296,7 +296,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "14399819-3953-4e6a-b8b6-e47ee7964a0e",
+										"id": "2b0aec01-082f-43bc-9a1c-ca6c54674f59",
 										"exec": [
 											"pm.test(\"Status code is 404\", function () {",
 											"    pm.response.to.have.status(404);",
@@ -336,7 +336,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "eb19af6e-d4bb-4175-bba2-6a26ddddb2c8",
+										"id": "9c50e96d-de13-475c-aa4a-cb5810e1d022",
 										"exec": [
 											"pm.test(\"Status code is 200\", function () {",
 											"    pm.response.to.have.status(200);",
@@ -404,7 +404,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "76b8a01e-8601-4208-b26e-134ca016ae6f",
+										"id": "c31e3f99-e58c-4938-a499-4b97e13e97b4",
 										"exec": [
 											"pm.test(\"Status code is 200\", function () {",
 											"    pm.response.to.have.status(200);",
@@ -415,7 +415,7 @@
 											"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig']);",
 											"    pm.expect(jsonData.patch.supported).to.eql(false);",
 											"    pm.expect(jsonData.bulk.supported).to.eql(false);",
-											"    pm.expect(jsonData.filter.supported).to.eql(false);",
+											"    pm.expect(jsonData.filter.supported).to.eql(true);",
 											"    pm.expect(jsonData.changePassword.supported).to.eql(false);",
 											"    pm.expect(jsonData.sort.supported).to.eql(false);",
 											"    pm.expect(jsonData.etag.supported).to.eql(false);",
@@ -456,7 +456,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "e9c4f550-8f7e-4250-8a98-157b1142ce9c",
+										"id": "64679166-ca71-4cda-ac00-62c1b9608496",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -496,7 +496,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "452ba5d3-3409-438a-b7fd-22ec9f93814e",
+										"id": "7d7e285a-6afa-4bd5-911e-e2ada096fbcd",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -542,7 +542,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "b4a8ab7c-aa71-4aed-9e28-7c51a14f40bc",
+										"id": "12099416-1259-48ae-844a-cfa599ce9ae8",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -586,7 +586,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "562cae06-8a00-4d89-88e4-ce1fafe1d197",
+										"id": "c9eaa9c0-1305-4d77-aa54-52bc72f261ba",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -636,7 +636,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "719db40a-2330-460e-af68-00a8078a87f6",
+										"id": "974fcb4e-d49e-4739-b1d2-5189fbd19699",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -681,7 +681,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "12f3700a-2b5a-4cea-aed8-207ce5c2701e",
+										"id": "585a71de-2ad5-4da5-98f5-a5eb02ce8c3a",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -732,7 +732,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "9069ae84-f7b9-420c-8f91-b4a36ddc6837",
+										"id": "0b37b536-7a5b-4779-be4f-b3b173f4d5f1",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -777,7 +777,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "05778014-f333-440b-8e8f-5781f159d322",
+										"id": "e647d977-407b-46e7-8dae-0b8afdaa2034",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -828,7 +828,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "57c9def2-9208-48e7-bacd-4eb60872eb34",
+										"id": "f22c38a7-7655-4f59-91a9-7ca1d952aef2",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -868,7 +868,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "7313b01e-b8d5-4355-a3cf-7eb9fa975d22",
+										"id": "b99c8f47-b121-435b-b737-c120e6ce430f",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -914,7 +914,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "21667bdb-0b40-40e7-9c9a-6eb9faebb1ae",
+										"id": "8575e744-e31b-4cac-98d3-ea3556a94512",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -959,7 +959,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "c4241935-ae71-40d4-83ac-822080b8a450",
+										"id": "8630bc58-17c2-4bb4-bf66-c341d3cc567b",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -1010,7 +1010,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "0ecea598-d840-4dd8-af37-1943f98f9d89",
+										"id": "02b40ffd-ca97-4a68-994d-c1744bc2784a",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -1054,7 +1054,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "e5d4a232-0895-422d-aac8-3bc67da10548",
+										"id": "2661bdf8-86ae-430f-abc0-b5371b519409",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -1104,7 +1104,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "2f8ca923-4eb7-4180-ad77-50c1cd0b7926",
+										"id": "9c9a2243-e68a-410d-a4ba-a3ca23e14880",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -1149,7 +1149,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "4c94970a-49ef-4667-b1e0-8d5fcd5702da",
+										"id": "4760f2f0-5b2d-411c-b5df-1234336278a6",
 										"exec": [
 											"pm.test(\"Status code is 401\", function () {",
 											"    pm.response.to.have.status(401);",
@@ -1207,7 +1207,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "eb384d65-f97d-472f-aff3-29a9fc6b2a07",
+										"id": "99537ebe-5110-46c3-a1df-d9b252f4829e",
 										"exec": [
 											"pm.test(\"Status code is 201\", function () {",
 											"    pm.response.to.have.status(201);",
@@ -1261,7 +1261,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "494c118a-a0ba-492c-a4b5-10bb2b12d682",
+										"id": "6c83794f-61e9-4a2e-bf43-17c693514942",
 										"exec": [
 											"pm.test(\"Status code is 200\", function () {",
 											"    pm.response.to.have.status(200);",
@@ -1316,7 +1316,7 @@
 								{
 									"listen": "test",
 									"script": {
-										"id": "7d65e443-124c-40a0-b1be-dadd758b616d",
+										"id": "9879ae22-edf6-41e4-b489-f9d773141c21",
 										"exec": [
 											"pm.test(\"Status code is 200\", function () {",
 											"    pm.response.to.have.status(200);",
@@ -1335,7 +1335,7 @@
 								{
 									"listen": "prerequest",
 									"script": {
-										"id": "802b16e1-a8a7-4721-8adc-a2dc0da461bf",
+										"id": "295b523a-d3f5-461b-be5e-48a622b292b6",
 										"exec": [
 											""
 										],
@@ -1386,7 +1386,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "f58f7b5a-b2e0-4f81-a103-5e7b532c5971",
+												"id": "22d6522c-1faf-4352-94a9-75d19f0bc5f2",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1437,7 +1437,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "1fa6e8fc-2884-4628-a2c3-be562146548f",
+												"id": "91654741-98a8-4a68-9405-d4fcc92b0fd0",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1494,7 +1494,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "8aba6653-50c1-4ea2-8479-2abc444f8673",
+												"id": "9c5a16f7-98d2-4e1d-a2a3-b50a78f05918",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1551,7 +1551,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "7a310cec-3a93-4129-872d-1738bd57d396",
+												"id": "7d32d73a-f41b-43c8-adf2-a7feb151f7ba",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1608,7 +1608,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "2af6c82d-dc43-4121-8c6e-08214606ec85",
+												"id": "9e2579da-0cfb-4aef-9d60-4c800643197f",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1665,7 +1665,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "579316fc-56ce-4757-bfe8-481c53593a73",
+												"id": "36c49e9a-1d6e-4324-81a9-546e36307d43",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1722,7 +1722,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "295791aa-4cef-4675-961e-b170ce63d893",
+												"id": "5e4fc3aa-1d92-4d5b-8a35-2a7f9d046287",
 												"exec": [
 													"pm.test(\"Status code is 201\", function () {",
 													"    pm.response.to.have.status(201);",
@@ -1778,7 +1778,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "0b78bce8-c2da-4155-aa7c-2e92186116f9",
+												"id": "8ebe4377-e69f-4448-aacd-cdddd7be7ced",
 												"exec": [
 													"pm.test(\"Status code is 409\", function () {",
 													"    pm.response.to.have.status(409);",
@@ -1842,7 +1842,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "51f82670-ce5f-4332-88fc-7f94f01e2b88",
+												"id": "b293c75e-aea0-4bce-a1e3-0af2e2ea26ae",
 												"exec": [
 													"pm.test(\"Status code is 404\", function () {",
 													"    pm.response.to.have.status(404);",
@@ -1899,7 +1899,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "423e57ff-3271-4738-8c04-5e1552686923",
+												"id": "856deb98-3101-45e8-8533-0427465fdb34",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -1951,7 +1951,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "4e4629cc-c797-4820-8fee-02d95af0fbb2",
+												"id": "752b862b-b24c-4020-b89d-3360a108e75d",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2015,7 +2015,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "73f5406c-afdc-4127-bb34-f98bd38f2540",
+												"id": "6d679b18-3945-4732-915e-340b8ea87d45",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2057,6 +2057,327 @@
 										}
 									},
 									"response": []
+								},
+								{
+									"name": "Filter users - invalid filter",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "ee863ad5-b818-478e-ae58-e75508d5bca3",
+												"exec": [
+													"pm.test(\"Status code is 400\", function () {",
+													"    pm.response.to.have.status(400);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - invalid filter\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:Error']);",
+													"    pm.expect(jsonData.scimType).to.eql('invalidSyntax');",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"value": "Bearer {{access_token}}",
+												"type": "text"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=invalid",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "invalid"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Filter users - user not found",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "f673a5b9-138d-4416-8645-8b28c637b83f",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - unknown user\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:ListResponse']);",
+													"    pm.expect(jsonData.totalResults).to.eql(0);",
+													"    pm.expect(jsonData.startIndex).to.eql(1);",
+													"    pm.expect(jsonData.itemsPerPage).to.eql(0);",
+													"    pm.expect(jsonData.Resources.length).to.eql(0);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{access_token}}"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=userName eq \"invalid\"",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "userName eq \"invalid\""
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Filter users - user found - simple filter - eq operand",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "525a17e7-afe9-4d8f-9a4a-fd35ca0193a6",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - simple filter - eq\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:ListResponse']);",
+													"    pm.expect(jsonData.totalResults).to.eql(1);",
+													"    pm.expect(jsonData.startIndex).to.eql(1);",
+													"    pm.expect(jsonData.itemsPerPage).to.eql(1);",
+													"    pm.expect(jsonData.Resources.length).to.eql(1);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{access_token}}"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=userName eq \"bjensen@example.com\"",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "userName eq \"bjensen@example.com\""
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Filter users - user found - simple filter - regex operand",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "1116aec3-a33a-4f08-a0d6-f74f9878c233",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - simple filter - regex\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:ListResponse']);",
+													"    pm.expect(jsonData.totalResults).to.eql(1);",
+													"    pm.expect(jsonData.startIndex).to.eql(1);",
+													"    pm.expect(jsonData.itemsPerPage).to.eql(1);",
+													"    pm.expect(jsonData.Resources.length).to.eql(1);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{access_token}}"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=userName co \"bjensen\"",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "userName co \"bjensen\""
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Filter users - user found - complex filter - or operand",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "581d832a-ae67-4279-9eaf-4aba2e7ba42a",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - complex filter - or\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:ListResponse']);",
+													"    pm.expect(jsonData.totalResults).to.eql(1);",
+													"    pm.expect(jsonData.startIndex).to.eql(1);",
+													"    pm.expect(jsonData.itemsPerPage).to.eql(1);",
+													"    pm.expect(jsonData.Resources.length).to.eql(1);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{access_token}}"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=userName eq \"invalid\" or name.givenName eq \"Barbara\"",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "userName eq \"invalid\" or name.givenName eq \"Barbara\""
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Filter users - user found - complex filter - and operand",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "4ddc7d4a-4526-48ad-b1c4-c2bfecef526b",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"SCIM list users - complex filter - and\", function () {",
+													"    var jsonData = pm.response.json();",
+													"    pm.expect(jsonData.schemas).to.eql(['urn:ietf:params:scim:api:messages:2.0:ListResponse']);",
+													"    pm.expect(jsonData.totalResults).to.eql(1);",
+													"    pm.expect(jsonData.startIndex).to.eql(1);",
+													"    pm.expect(jsonData.itemsPerPage).to.eql(1);",
+													"    pm.expect(jsonData.Resources.length).to.eql(1);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "GET",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{access_token}}"
+											}
+										],
+										"url": {
+											"raw": "{{gateway_url}}/{{domain}}/scim/Users?filter=name.givenName eq \"Barbara\" and active eq false and meta.created ge \"2020-10-14T20:18:43.048Z\"",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"{{domain}}",
+												"scim",
+												"Users"
+											],
+											"query": [
+												{
+													"key": "filter",
+													"value": "name.givenName eq \"Barbara\" and active eq false and meta.created ge \"2020-10-14T20:18:43.048Z\""
+												}
+											]
+										}
+									},
+									"response": []
 								}
 							],
 							"protocolProfileBehavior": {},
@@ -2071,7 +2392,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "370ba785-dea5-4809-8cb2-00c91d0c2f92",
+												"id": "2fdcae98-8588-4e9d-90cb-65322a0115dc",
 												"exec": [
 													"pm.test(\"Status code is 404\", function () {",
 													"    pm.response.to.have.status(404);",
@@ -2122,7 +2443,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "b741b970-caf1-4d1e-bc9a-4a8a2fbcaf38",
+												"id": "554966aa-b64f-488b-973e-b8e90a21e5ab",
 												"exec": [
 													"pm.test(\"Status code is 204\", function () {",
 													"    pm.response.to.have.status(204);",
@@ -2166,7 +2487,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "74f8fcbb-1d27-41ec-a6e9-f9e4884fe5ee",
+												"id": "eeea7a6c-0a93-40f3-a503-6d493d9ea41b",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2228,7 +2549,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "61cf0482-1c53-46ea-b0c5-30864c79d02a",
+												"id": "154a610f-88c6-4d6f-8dd1-7ef5a43a3d19",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -2279,7 +2600,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "dee423fa-70a0-4014-a8eb-c40d5c281780",
+												"id": "a5b38152-7c85-4b3d-a473-261297e58a52",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -2336,7 +2657,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "769e8452-b0a6-4b82-a794-06b14bc675e3",
+												"id": "df09b87c-c019-485f-a2c7-c504a913bf95",
 												"exec": [
 													"pm.test(\"Status code is 201\", function () {",
 													"    pm.response.to.have.status(201);",
@@ -2392,7 +2713,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "c2aa651a-581d-46f6-9e29-f3cb5f33085a",
+												"id": "0e8a8d8b-a5df-4522-ac3d-5d7be45a6ac8",
 												"exec": [
 													"pm.test(\"Status code is 409\", function () {",
 													"    pm.response.to.have.status(409);",
@@ -2456,7 +2777,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "7c886a2f-c322-4ac5-85b1-dac15b0814ea",
+												"id": "b9200be7-4108-4ae5-b4c7-5102b85b881c",
 												"exec": [
 													"pm.test(\"Status code is 404\", function () {",
 													"    pm.response.to.have.status(404);",
@@ -2513,7 +2834,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "d091744c-01bb-4776-829f-b4176dd279c1",
+												"id": "12203bb6-20f6-4c0f-994b-872e7a1c202e",
 												"exec": [
 													"pm.test(\"Status code is 400\", function () {",
 													"    pm.response.to.have.status(400);",
@@ -2565,7 +2886,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "072058cf-dc72-4d48-8dec-8af94ed6546a",
+												"id": "c4b9c502-6131-489a-97c1-726df18e046a",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2629,7 +2950,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "05fecee1-acc7-4765-b027-2e42cf043d25",
+												"id": "294bede6-b9f4-4bc5-8ef9-9c87828763b3",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2684,7 +3005,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "3ade713b-c009-4ac7-9112-32b6de7313da",
+												"id": "e5d31a3d-1716-4caf-a757-d2fad1dd3989",
 												"exec": [
 													"pm.test(\"Status code is 404\", function () {",
 													"    pm.response.to.have.status(404);",
@@ -2735,7 +3056,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "fa910be6-a752-412f-8378-6e960c95a471",
+												"id": "31393e7d-0f87-48ab-a89e-2a99497539bd",
 												"exec": [
 													"pm.test(\"Status code is 204\", function () {",
 													"    pm.response.to.have.status(204);",
@@ -2779,7 +3100,7 @@
 										{
 											"listen": "test",
 											"script": {
-												"id": "ddd7d501-764f-4c1d-922f-362989bd3bf8",
+												"id": "b0c39cc2-82e4-4007-8a04-8635e3ae9036",
 												"exec": [
 													"pm.test(\"Status code is 200\", function () {",
 													"    pm.response.to.have.status(200);",
@@ -2838,7 +3159,7 @@
 				{
 					"listen": "test",
 					"script": {
-						"id": "3ec075a8-33a9-48fb-b4cb-5f19d2c4ca73",
+						"id": "dfe99dc6-cf9c-466d-96ff-7e3fd8972d2d",
 						"exec": [
 							"pm.test(\"Status code is 204\", function () {",
 							"    pm.response.to.have.status(204);",


Test patch:
diff --git a/gravitee-am-common/src/test/java/io/gravitee/am/common/scim/parser/SCIMFilterParserTest.java b/gravitee-am-common/src/test/java/io/gravitee/am/common/scim/parser/SCIMFilterParserTest.java
new file mode 100644
index 0000000000..b2c6fb6744
--- /dev/null
+++ b/gravitee-am-common/src/test/java/io/gravitee/am/common/scim/parser/SCIMFilterParserTest.java
@@ -0,0 +1,67 @@
+/**
+ * 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.common.scim.parser;
+
+import io.gravitee.am.common.scim.filter.Filter;
+import io.gravitee.am.common.scim.filter.Operator;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SCIMFilterParserTest {
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldNotParse_invalidFilter() {
+        SCIMFilterParser.parse("invalid-value");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldNotParse_invalidFilter2() {
+        SCIMFilterParser.parse("userName eq");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldNotParse_invalidFilter3() {
+        SCIMFilterParser.parse("userName eq \"test\" or");
+    }
+
+    @Test
+    public void shouldParse_compose_filter() {
+        Filter filter = SCIMFilterParser.parse("userType eq 1.99 and (emails co \"example.com\" or emails.value co \"example.org\")");
+        assertNotNull(filter);
+        assertNotNull(filter.getFilterComponents());
+        // global operator should be "and"
+        assertTrue(filter.getOperator().equals(Operator.AND));
+        // left operator should be "eq"
+        assertTrue(filter.getFilterComponents().get(0).getOperator().equals(Operator.EQUALITY));
+        // right operator should be "or"
+        assertTrue(filter.getFilterComponents().get(1).getOperator().equals(Operator.OR));
+    }
+
+    @Test
+    public void shouldParse_present_filter() {
+        Filter filter = SCIMFilterParser.parse("title pr");
+        assertNotNull(filter);
+        assertNull(filter.getFilterComponents());
+        assertTrue(filter.getOperator().equals(Operator.PRESENCE));
+        assertTrue("title".equals(filter.getFilterAttribute().toString()));
+        assertNull(filter.getFilterValue());
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/test/java/io/gravitee/am/gateway/handler/scim/resources/users/UsersEndpointTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/test/java/io/gravitee/am/gateway/handler/scim/resources/users/UsersEndpointTest.java
new file mode 100644
index 0000000000..d593c5ed4d
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-scim/src/test/java/io/gravitee/am/gateway/handler/scim/resources/users/UsersEndpointTest.java
@@ -0,0 +1,113 @@
+/**
+ * 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.scim.resources.users;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import io.gravitee.am.gateway.handler.common.vertx.RxWebTestBase;
+import io.gravitee.am.gateway.handler.scim.model.ListResponse;
+import io.gravitee.am.gateway.handler.scim.resources.ErrorHandler;
+import io.gravitee.am.gateway.handler.scim.service.UserService;
+import io.gravitee.am.common.scim.filter.Filter;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
+import io.reactivex.Single;
+import io.vertx.core.http.HttpMethod;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class UsersEndpointTest extends RxWebTestBase {
+
+    @Mock
+    private UserService userService;
+
+    @Mock
+    private PasswordValidator passwordValidator;
+
+    @Mock
+    private ObjectMapper objectMapper;
+
+    @Mock
+    private ObjectWriter objectWriter;
+
+    @InjectMocks
+    private UsersEndpoint usersEndpoint = new UsersEndpoint(userService, objectMapper, passwordValidator);
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        // object mapper
+        when(objectWriter.writeValueAsString(any())).thenReturn("UserObject");
+        when(objectMapper.writerWithDefaultPrettyPrinter()).thenReturn(objectWriter);
+
+        router.get("/Users")
+                .handler(usersEndpoint::list)
+                .failureHandler(new ErrorHandler());
+    }
+
+    @Test
+    public void shouldListUsers() throws Exception {
+        when(userService.list(eq(null), eq(0), eq(100), anyString())).thenReturn(Single.just(new ListResponse<>()));
+        testRequest(
+                HttpMethod.GET,
+                "/Users",
+                req -> {},
+                200,
+                "OK",
+                "UserObject");
+    }
+
+    @Test
+    public void shouldNotListUsers_invalidFilter() throws Exception {
+        testRequest(
+                HttpMethod.GET,
+                "/Users?filter=wrong",
+                req -> {},
+                400,
+                "Bad Request",
+                "{\n" +
+                        "  \"status\" : \"400\",\n" +
+                        "  \"scimType\" : \"invalidSyntax\",\n" +
+                        "  \"detail\" : \"Invalid filter 'wrong': End of input at position 5 but expected an attribute operator\",\n" +
+                        "  \"schemas\" : [ \"urn:ietf:params:scim:api:messages:2.0:Error\" ]\n" +
+                        "}");
+    }
+
+    @Test
+    public void shouldListUsers_validFilter() throws Exception {
+        when(userService.list(any(Filter.class), eq(0), eq(100), anyString())).thenReturn(Single.just(new ListResponse<>()));
+        testRequest(
+                HttpMethod.GET,
+                "/Users?filter=userName%20eq%20%22bjensen%22",
+                request -> {
+                    request.query();
+                },
+                200,
+                "OK",
+                "UserObject");
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-rest/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UsersResourceTest.java b/gravitee-am-management-api/gravitee-am-management-api-rest/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UsersResourceTest.java
index 57b8aab677..c18217be75 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-rest/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UsersResourceTest.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-rest/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UsersResourceTest.java
@@ -17,9 +17,9 @@ package io.gravitee.am.management.handlers.management.api.resources;
 
 import io.gravitee.am.management.handlers.management.api.JerseySpringTest;
 import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.model.User;
 import io.gravitee.am.model.common.Page;
-import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.service.exception.TechnicalManagementException;
 import io.gravitee.am.service.exception.UserProviderNotFoundException;
 import io.gravitee.am.service.model.NewUser;
@@ -67,7 +67,7 @@ public class UsersResourceTest extends JerseySpringTest {
         final Page<User> pagedUsers = new Page<>(users, 0, 2);
 
         doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
-        doReturn(Single.just(pagedUsers)).when(userService).findByDomain(domainId, 0, 10);
+        doReturn(Single.just(pagedUsers)).when(userService).findAll(ReferenceType.DOMAIN, domainId, 0, 10);
 
         final Response response = target("domains")
                 .path(domainId)
@@ -80,6 +80,26 @@ public class UsersResourceTest extends JerseySpringTest {
         assertEquals(HttpStatusCode.OK_200, response.getStatus());
     }
 
+    @Test
+    public void shouldNotSearchUsers_invalidFilter() {
+        final String domainId = "domain-1";
+        final Domain mockDomain = new Domain();
+        mockDomain.setId(domainId);
+
+        doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("users")
+                .queryParam("filter", "invalid-filter")
+                .queryParam("page", 0)
+                .queryParam("size", 10)
+                .request()
+                .get();
+
+        assertEquals(HttpStatusCode.BAD_REQUEST_400, response.getStatus());
+    }
+
     @Test
     public void shouldGetUsers_technicalManagementException() {
         final String domainId = "domain-1";
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/common/FilterCriteriaParserTest.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/common/FilterCriteriaParserTest.java
new file mode 100644
index 0000000000..0715776fcb
--- /dev/null
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/common/FilterCriteriaParserTest.java
@@ -0,0 +1,96 @@
+/**
+ * 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.repository.mongodb.common;
+
+import io.gravitee.am.repository.management.api.search.FilterCriteria;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * @author Titouan COMPIEGNE (david.brassely at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class FilterCriteriaParserTest {
+
+    @Test
+    public void shouldParse_eq_criteria() {
+        FilterCriteria filterCriteria = new FilterCriteria();
+        filterCriteria.setOperator("eq");
+        filterCriteria.setFilterName("username");
+        filterCriteria.setFilterValue("Alice");
+        filterCriteria.setQuoteFilterValue(true);
+
+        String query = FilterCriteriaParser.parse(filterCriteria);
+        Assert.assertEquals("{\"username\":{$eq:\"Alice\"}}", query);
+    }
+
+    @Test
+    public void shouldParse_exists_criteria() {
+        FilterCriteria filterCriteria = new FilterCriteria();
+        filterCriteria.setOperator("pr");
+        filterCriteria.setFilterName("username");
+
+        String query = FilterCriteriaParser.parse(filterCriteria);
+        Assert.assertEquals("{\"username\":{$exists:true}}", query);
+    }
+
+    @Test
+    public void shouldParse_nested_criteria() {
+        FilterCriteria filterCriteria = new FilterCriteria();
+        filterCriteria.setOperator("eq");
+        filterCriteria.setFilterName("name.username");
+        filterCriteria.setFilterValue("Alice");
+        filterCriteria.setQuoteFilterValue(true);
+
+        String query = FilterCriteriaParser.parse(filterCriteria);
+        Assert.assertEquals("{\"name.username\":{$eq:\"Alice\"}}", query);
+    }
+
+    @Test
+    public void shouldParse_compose_criteria() {
+        FilterCriteria filterCriteria = new FilterCriteria();
+        filterCriteria.setOperator("and");
+
+        FilterCriteria leftPart = new FilterCriteria();
+        leftPart.setOperator("eq");
+        leftPart.setFilterName("username");
+        leftPart.setFilterValue("Alice");
+        leftPart.setQuoteFilterValue(true);
+
+        FilterCriteria rightPart = new FilterCriteria();
+        rightPart.setOperator("or");
+
+        FilterCriteria rightLeftPart = new FilterCriteria();
+        rightLeftPart.setOperator("co");
+        rightLeftPart.setFilterName("email");
+        rightLeftPart.setFilterValue("Alice");
+        rightLeftPart.setQuoteFilterValue(true);
+
+        FilterCriteria rightRightPart = new FilterCriteria();
+        rightRightPart.setOperator("sw");
+        rightRightPart.setFilterName("nickname");
+        rightRightPart.setFilterValue("Alice");
+        rightRightPart.setQuoteFilterValue(true);
+
+        rightPart.setFilterComponents(Arrays.asList(rightLeftPart, rightRightPart));
+        filterCriteria.setFilterComponents(Arrays.asList(leftPart, rightPart));
+
+        String query = FilterCriteriaParser.parse(filterCriteria);
+        Assert.assertEquals("{$and:[{\"username\":{$eq:\"Alice\"}},{$or:[{\"email\":{$regex:\"Alice\",$options:\"i\"}},{\"nickname\":{$regex:\"^Alice\",$options:\"i\"}}]}]}", query);
+    }
+}
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 f094bc468b..3fa824d513 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
@@ -16,9 +16,9 @@
 package io.gravitee.am.repository.mongodb.management;
 
 import io.gravitee.am.common.oidc.StandardClaims;
+import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.model.User;
 import io.gravitee.am.model.common.Page;
-import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.repository.exceptions.TechnicalException;
 import io.gravitee.am.repository.management.api.UserRepository;
 import io.reactivex.observers.TestObserver;
@@ -168,59 +168,57 @@ public class MongoUserRepositoryTest extends AbstractManagementRepositoryTest {
     }
 
     @Test
-    public void testSearch_strict() {
-        final String domain = "domain";
-        // create user
-        User user = new User();
-        user.setReferenceType(ReferenceType.DOMAIN);
-        user.setReferenceId(domain);
-        user.setUsername("testUsername");
-        userRepository.create(user).blockingGet();
+    public void testSearch_byUsername_strict() {
+        testSearch_strict("testUsername");
+    }
 
-        User user2 = new User();
-        user2.setReferenceType(ReferenceType.DOMAIN);
-        user2.setReferenceId(domain);
-        user2.setUsername("testUsername2");
-        userRepository.create(user2).blockingGet();
+    @Test
+    public void testSearch_byDisplayName_strict() {
+        testSearch_strict("displayName");
+    }
 
-        // fetch user
-        TestObserver<Page<User>> testObserver = userRepository.search(domain, "testUsername", 0, 10).test();
-        testObserver.awaitTerminalEvent();
+    @Test
+    public void testSearch_byFirstName_strict() {
+        testSearch_strict("firstName");
+    }
 
-        testObserver.assertComplete();
-        testObserver.assertNoErrors();
-        testObserver.assertValue(users -> users.getData().size() == 1);
-        testObserver.assertValue(users -> users.getData().iterator().next().getUsername().equals(user.getUsername()));
+    @Test
+    public void testSearch_byLastName_strict() {
+        testSearch_strict("lastName");
+    }
 
+    @Test
+    public void testSearch_email_strict() {
+        testSearch_strict("user.name@mail.com");
     }
 
     @Test
-    public void testSearch_wildcard() {
-        final String domain = "domain";
-        // create user
-        User user = new User();
-        user.setReferenceType(ReferenceType.DOMAIN);
-        user.setReferenceId(domain);
-        user.setUsername("testUsername");
-        userRepository.create(user).blockingGet();
+    public void testSearch_byUsername_wildcard() {
+        testSearch_wildcard("testUsername*");
+    }
 
-        User user2 = new User();
-        user2.setReferenceType(ReferenceType.DOMAIN);
-        user2.setReferenceId(domain);
-        user2.setUsername("testUsername2");
-        userRepository.create(user2).blockingGet();
+    @Test
+    public void testSearch_byDisplayName_wildcard() {
+        testSearch_wildcard("displayName*");
+    }
 
-        // fetch user
-        TestObserver<Page<User>> testObserver = userRepository.search(domain, "testUsername*", 0, 10).test();
-        testObserver.awaitTerminalEvent();
+    @Test
+    public void testSearch_byFirstName_wildcard() {
+        testSearch_wildcard("firstName*");
+    }
 
-        testObserver.assertComplete();
-        testObserver.assertNoErrors();
-        testObserver.assertValue(users -> users.getData().size() == 2);
+    @Test
+    public void testSearch_byLastName_wildcard() {
+        testSearch_wildcard("lastName*");
+    }
+
+    @Test
+    public void testSearch_email_wildcard() {
+        testSearch_wildcard("user.name@mail.com*");
     }
 
     @Test
-    public void testSearch_paged() {
+    public void testSearch_byUsername_paged() {
         final String domain = "domain";
         // create user
         User user1 = new User();
@@ -242,7 +240,7 @@ public class MongoUserRepositoryTest extends AbstractManagementRepositoryTest {
         userRepository.create(user3).blockingGet();
 
         // fetch user (page 0)
-        TestObserver<Page<User>> testObserverP0 = userRepository.search(domain, "testUsername*", 0, 2).test();
+        TestObserver<Page<User>> testObserverP0 = userRepository.search(ReferenceType.DOMAIN, domain, "testUsername*", 0, 2).test();
         testObserverP0.awaitTerminalEvent();
 
         testObserverP0.assertComplete();
@@ -254,7 +252,7 @@ public class MongoUserRepositoryTest extends AbstractManagementRepositoryTest {
         });
 
         // fetch user (page 1)
-        TestObserver<Page<User>> testObserverP1 = userRepository.search(domain, "testUsername*", 1, 2).test();
+        TestObserver<Page<User>> testObserverP1 = userRepository.search(ReferenceType.DOMAIN, domain, "testUsername*", 1, 2).test();
         testObserverP1.awaitTerminalEvent();
 
         testObserverP1.assertComplete();
@@ -312,4 +310,70 @@ public class MongoUserRepositoryTest extends AbstractManagementRepositoryTest {
         testObserver.assertValue(users -> users.size() == 2);
     }
 
+    private void testSearch_strict(String query) {
+        final String domain = "domain";
+        // create user
+        User user = new User();
+        user.setReferenceType(ReferenceType.DOMAIN);
+        user.setReferenceId(domain);
+        user.setFirstName("firstName");
+        user.setLastName("lastName");
+        user.setDisplayName("displayName");
+        user.setUsername("testUsername");
+        user.setEmail("user.name@mail.com");
+        userRepository.create(user).blockingGet();
+
+        User user2 = new User();
+        user2.setReferenceType(ReferenceType.DOMAIN);
+        user2.setReferenceId(domain);
+        user2.setFirstName("firstName2");
+        user2.setLastName("lastName2");
+        user2.setDisplayName("displayName2");
+        user2.setUsername("testUsername2");
+        user2.setEmail("user.name@mail.com2");
+        userRepository.create(user2).blockingGet();
+
+        // fetch user
+        TestObserver<Page<User>> testObserver = userRepository.search(ReferenceType.DOMAIN, domain, query, 0, 10).test();
+        testObserver.awaitTerminalEvent();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValue(users -> users.getData().size() == 1);
+        testObserver.assertValue(users -> users.getData().iterator().next().getUsername().equals(user.getUsername()));
+
+    }
+
+    private void testSearch_wildcard(String query) {
+        final String domain = "domain";
+        // create user
+        User user = new User();
+        user.setReferenceType(ReferenceType.DOMAIN);
+        user.setReferenceId(domain);
+        user.setFirstName("firstName");
+        user.setLastName("lastName");
+        user.setDisplayName("displayName");
+        user.setUsername("testUsername");
+        user.setEmail("user.name@mail.com");
+        userRepository.create(user).blockingGet();
+
+        User user2 = new User();
+        user2.setReferenceType(ReferenceType.DOMAIN);
+        user2.setReferenceId(domain);
+        user2.setFirstName("firstName2");
+        user2.setLastName("lastName2");
+        user2.setDisplayName("displayName2");
+        user2.setUsername("testUsername2");
+        user2.setEmail("user.name@mail.com2");
+        userRepository.create(user2).blockingGet();
+
+        // fetch user
+        TestObserver<Page<User>> testObserver = userRepository.search(ReferenceType.DOMAIN, domain, query, 0, 10).test();
+        testObserver.awaitTerminalEvent();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValue(users -> users.getData().size() == 2);
+    }
+
 }