diff --git a/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx
index 1274f30a206e91a2c29e37612dab584ce2f95e44..35846c6e1f3529b17b50234c6690e456e47512d6 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx
@@ -30,6 +30,7 @@ type State = {
   showAddRow: boolean,
   groups: Object[],
   mappings: { [string]: number[] },
+  saveError: ?Object,
 };
 
 export default class LdapGroupMappingsWidget extends React.Component {
@@ -43,6 +44,7 @@ export default class LdapGroupMappingsWidget extends React.Component {
       showAddRow: false,
       groups: [],
       mappings: {},
+      saveError: null,
     };
   }
 
@@ -117,13 +119,24 @@ export default class LdapGroupMappingsWidget extends React.Component {
     SettingsApi.put({ key: "ldap-group-mappings", value: mappings }).then(
       () => {
         onChangeSetting("ldap-group-mappings", mappings);
-        this.setState({ showEditModal: false, showAddRow: false });
+        this.setState({
+          showEditModal: false,
+          showAddRow: false,
+          saveError: null,
+        });
       },
+      e => this.setState({ saveError: e }),
     );
   };
 
   render() {
-    const { showEditModal, showAddRow, groups, mappings } = this.state;
+    const {
+      showEditModal,
+      showAddRow,
+      groups,
+      mappings,
+      saveError,
+    } = this.state;
 
     return (
       <div className="flex align-center">
@@ -178,6 +191,11 @@ export default class LdapGroupMappingsWidget extends React.Component {
                 </AdminContentTable>
               </div>
               <ModalFooter>
+                {saveError && saveError.data && saveError.data.message ? (
+                  <span className="text-error text-bold">
+                    {saveError.data.message}
+                  </span>
+                ) : null}
                 <Button
                   type="button"
                   onClick={this._cancelClick}
diff --git a/src/metabase/integrations/ldap.clj b/src/metabase/integrations/ldap.clj
index 89453b4844690de0777325b56b636e021f48a3f0..a7593b6218ccff9ab373ed1bdb673ce2f5970c98 100644
--- a/src/metabase/integrations/ldap.clj
+++ b/src/metabase/integrations/ldap.clj
@@ -1,5 +1,6 @@
 (ns metabase.integrations.ldap
-  (:require [clj-ldap.client :as ldap]
+  (:require [cheshire.core :as json]
+            [clj-ldap.client :as ldap]
             [clojure
              [set :as set]
              [string :as str]]
@@ -10,7 +11,7 @@
             [metabase.util :as u]
             [metabase.util.i18n :refer [tru]]
             [toucan.db :as db])
-  (:import [com.unboundid.ldap.sdk LDAPConnectionPool LDAPException]))
+  (:import [com.unboundid.ldap.sdk DN Filter LDAPConnectionPool LDAPException]))
 
 (def ^:private filter-placeholder
   "{login}")
@@ -70,10 +71,17 @@
   (tru "Search base for groups, not required if your LDAP directory provides a ''memberOf'' overlay. (Will be searched recursively)"))
 
 (defsetting ldap-group-mappings
-  ;; Should be in the form: {"cn=Some Group,dc=...": [1, 2, 3]} where keys are LDAP groups and values are lists of MB groups IDs
+  ;; Should be in the form: {"cn=Some Group,dc=...": [1, 2, 3]} where keys are LDAP group DNs and values are lists of MB groups IDs
   (tru "JSON containing LDAP to Metabase group mappings.")
   :type    :json
-  :default {})
+  :default {}
+  :getter  (fn []
+             (json/parse-string (setting/get-string :ldap-group-mappings) #(DN. (str %))))
+  :setter  (fn [new-value]
+             (doseq [k (keys new-value)]
+               (when-not (DN/isValidDN (name k))
+                 (throw (IllegalArgumentException. (str (tru "{0} is not a valid DN." (name k)))))))
+             (setting/set-json! :ldap-group-mappings new-value)))
 
 (defn ldap-configured?
   "Check if LDAP is enabled and that the mandatory settings are configured."
@@ -96,11 +104,6 @@
                           :password  (ldap-password)
                           :security  (ldap-security)}))
 
-(defn- escape-value
-  "Escapes a value for use in an LDAP filter expression."
-  [value]
-  (str/replace value #"(?:^\s|\s$|[,\\\#\+<>;\"=\*\(\)\\0])" (comp (partial format "\\%02X") int first)))
-
 (defn- get-connection
   "Connects to LDAP with the currently set settings and returns the connection."
   ^LDAPConnectionPool []
@@ -116,23 +119,20 @@
   "Will translate a set of DNs to a set of MB group IDs using the configured mappings."
   [ldap-groups]
   (-> (ldap-group-mappings)
-      (select-keys (map keyword ldap-groups))
+      (select-keys (map #(DN. (str %)) ldap-groups))
       vals
       flatten
       set))
 
 (defn- get-user-groups
   "Retrieve groups for a supplied DN."
-  ([dn]
+  ([^String dn]
     (with-connection get-user-groups dn))
-  ([conn dn]
+  ([conn ^String dn]
     (when (ldap-group-base)
-      (let [results (ldap/search conn (ldap-group-base) {:scope      :sub
-                                                         :filter     (str "member=" (escape-value dn))
-                                                         :attributes [:dn :distinguishedName]})]
-        (filter some?
-          (for [result results]
-            (or (:dn result) (:distinguishedName result))))))))
+      (let [results (ldap/search conn (ldap-group-base) {:scope  :sub
+                                                         :filter (Filter/createEqualityFilter "member" dn)})]
+        (map :dn results)))))
 
 (def ^:private user-base-error  {:status :ERROR, :message "User search base does not exist or is unreadable"})
 (def ^:private group-base-error {:status :ERROR, :message "Group search base does not exist or is unreadable"})
@@ -171,22 +171,22 @@
 
 (defn find-user
   "Gets user information for the supplied username."
-  ([username]
+  ([^String username]
     (with-connection find-user username))
-  ([conn username]
+  ([conn ^String username]
     (let [fname-attr (keyword (ldap-attribute-firstname))
           lname-attr (keyword (ldap-attribute-lastname))
           email-attr (keyword (ldap-attribute-email))]
       (when-let [[result] (ldap/search conn (ldap-user-base) {:scope      :sub
-                                                              :filter     (str/replace (ldap-user-filter) filter-placeholder (escape-value username))
-                                                              :attributes [:dn :distinguishedName fname-attr lname-attr email-attr :memberOf]
+                                                              :filter     (str/replace (ldap-user-filter) filter-placeholder (Filter/encodeValue username))
+                                                              :attributes [fname-attr lname-attr email-attr :memberOf]
                                                               :size-limit 1})]
-        (let [dn    (or (:dn result) (:distinguishedName result))
+        (let [dn    (:dn result)
               fname (get result fname-attr)
               lname (get result lname-attr)
               email (get result email-attr)]
           ;; Make sure we got everything as these are all required for new accounts
-          (when-not (or (empty? dn) (empty? fname) (empty? lname) (empty? email))
+          (when-not (some empty? [fname lname email])
             ;; ActiveDirectory (and others?) will supply a `memberOf` overlay attribute for groups
             ;; Otherwise we have to make the inverse query to get them
             (let [groups (when (ldap-group-sync)
diff --git a/test/metabase/integrations/ldap_test.clj b/test/metabase/integrations/ldap_test.clj
index 36878abf2a1fdb5b91bb48513c38f23a3c0130c5..eae1b63d34d0c58c736016a29611e133f0456b85 100644
--- a/test/metabase/integrations/ldap_test.clj
+++ b/test/metabase/integrations/ldap_test.clj
@@ -1,7 +1,8 @@
 (ns metabase.integrations.ldap-test
   (:require [expectations :refer [expect]]
             [metabase.integrations.ldap :as ldap]
-            [metabase.test.integrations.ldap :as ldap.test]))
+            [metabase.test.integrations.ldap :as ldap.test]
+            [metabase.test.util :as tu]))
 
 (defn- get-ldap-details []
   {:host       "localhost"
@@ -14,14 +15,6 @@
 
 ;; See test_resources/ldap.ldif for fixtures
 
-(expect
-  "\\20\\2AJohn \\28Dude\\29 Doe\\5C"
-  (#'ldap/escape-value " *John (Dude) Doe\\"))
-
-(expect
-  "John\\2BSmith@metabase.com"
-  (#'ldap/escape-value "John+Smith@metabase.com"))
-
 ;; The connection test should pass with valid settings
 (expect
   {:status :SUCCESS}
@@ -111,3 +104,11 @@
    :groups     ["cn=Accounting,ou=Groups,dc=metabase,dc=com"]}
   (ldap.test/with-ldap-server
     (ldap/find-user "John.Smith@metabase.com")))
+
+;; LDAP group matching should identify Metabase groups using DN equality rules
+(expect
+  #{1 2 3}
+  (tu/with-temporary-setting-values [ldap-group-mappings {"cn=accounting,ou=groups,dc=metabase,dc=com" [1 2]
+                                                          "cn=shipping,ou=groups,dc=metabase,dc=com" [2 3]}]
+    (#'ldap/ldap-groups->mb-group-ids ["CN=Accounting,OU=Groups,DC=metabase,DC=com"
+                                       "CN=Shipping,OU=Groups,DC=metabase,DC=com"])))