diff --git a/modules/auth/auth_form.go b/modules/auth/auth_form.go
index 7fc62607e5..1d02c7acf3 100644
--- a/modules/auth/auth_form.go
+++ b/modules/auth/auth_form.go
@@ -30,6 +30,11 @@ type AuthenticationForm struct {
 	SearchPageSize                int
 	Filter                        string
 	AdminFilter                   string
+	GroupsEnabled                 bool
+	GroupDN                       string
+	GroupFilter                   string
+	GroupMemberUID                string
+	UserUID                       string
 	RestrictedFilter              string
 	AllowDeactivateAll            bool
 	IsActive                      bool
diff --git a/modules/auth/ldap/README.md b/modules/auth/ldap/README.md
index 4f7961da6b..76841f44ae 100644
--- a/modules/auth/ldap/README.md
+++ b/modules/auth/ldap/README.md
@@ -103,3 +103,21 @@ share the following fields:
       matching parameter will be substituted with the user's username.
     * Example: (&(objectClass=posixAccount)(cn=%s))
     * Example: (&(objectClass=posixAccount)(uid=%s))
+
+**Verify group membership in LDAP** uses the following fields:
+
+* Group Search Base (optional)
+    * The LDAP DN used for groups.
+    * Example: ou=group,dc=mydomain,dc=com
+
+* Group Name Filter (optional)
+    * An LDAP filter declaring how to find valid groups in the above DN.
+    * Example: (|(cn=gitea_users)(cn=admins))
+
+* User Attribute in Group (optional)
+    * Which user LDAP attribute is listed in the group.
+    * Example: uid
+
+* Group Attribute for User (optional)
+    * Which group LDAP attribute contains an array above user attribute names.
+    * Example: memberUid
diff --git a/modules/auth/ldap/ldap.go b/modules/auth/ldap/ldap.go
index 66676f2829..7649639d36 100644
--- a/modules/auth/ldap/ldap.go
+++ b/modules/auth/ldap/ldap.go
@@ -1,4 +1,5 @@
 // Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2020 The Gitea Authors. All rights reserved.
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
@@ -13,7 +14,7 @@ import (
 
 	"code.gitea.io/gitea/modules/log"
 
-	ldap "gopkg.in/ldap.v3"
+	"gopkg.in/ldap.v3"
 )
 
 // SecurityProtocol protocol type
@@ -49,6 +50,11 @@ type Source struct {
 	RestrictedFilter      string // Query filter to check if user is restricted
 	Enabled               bool   // if this source is disabled
 	AllowDeactivateAll    bool   // Allow an empty search response to deactivate all users from this source
+	GroupsEnabled         bool   // if the group checking is enabled
+	GroupDN               string // Group Search Base
+	GroupFilter           string // Group Name Filter
+	GroupMemberUID        string // Group Attribute containing array of UserUID
+	UserUID               string // User Attribute listed in Group
 }
 
 // SearchResult : user data
@@ -84,6 +90,28 @@ func (ls *Source) sanitizedUserDN(username string) (string, bool) {
 	return fmt.Sprintf(ls.UserDN, username), true
 }
 
+func (ls *Source) sanitizedGroupFilter(group string) (string, bool) {
+	// See http://tools.ietf.org/search/rfc4515
+	badCharacters := "\x00*\\"
+	if strings.ContainsAny(group, badCharacters) {
+		log.Trace("Group filter invalid query characters: %s", group)
+		return "", false
+	}
+
+	return group, true
+}
+
+func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) {
+	// See http://tools.ietf.org/search/rfc4514: "special characters"
+	badCharacters := "\x00()*\\'\"#+;<>"
+	if strings.ContainsAny(groupDn, badCharacters) || strings.HasPrefix(groupDn, " ") || strings.HasSuffix(groupDn, " ") {
+		log.Trace("Group DN contains invalid query characters: %s", groupDn)
+		return "", false
+	}
+
+	return groupDn, true
+}
+
 func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
 	log.Trace("Search for LDAP user: %s", name)
 
@@ -279,11 +307,14 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
 	var isAttributeSSHPublicKeySet = len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
 
 	attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
+	if len(strings.TrimSpace(ls.UserUID)) > 0 {
+		attribs = append(attribs, ls.UserUID)
+	}
 	if isAttributeSSHPublicKeySet {
 		attribs = append(attribs, ls.AttributeSSHPublicKey)
 	}
 
-	log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, userFilter, userDN)
+	log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.UserUID, userFilter, userDN)
 	search := ldap.NewSearchRequest(
 		userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
 		attribs, nil)
@@ -308,6 +339,51 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
 	firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
 	surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
 	mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
+	uid := sr.Entries[0].GetAttributeValue(ls.UserUID)
+
+	// Check group membership
+	if ls.GroupsEnabled {
+		groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter)
+		if !ok {
+			return nil
+		}
+		groupDN, ok := ls.sanitizedGroupDN(ls.GroupDN)
+		if !ok {
+			return nil
+		}
+
+		log.Trace("Fetching groups '%v' with filter '%s' and base '%s'", ls.GroupMemberUID, groupFilter, groupDN)
+		groupSearch := ldap.NewSearchRequest(
+			groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter,
+			[]string{ls.GroupMemberUID},
+			nil)
+
+		srg, err := l.Search(groupSearch)
+		if err != nil {
+			log.Error("LDAP group search failed: %v", err)
+			return nil
+		} else if len(srg.Entries) < 1 {
+			log.Error("LDAP group search failed: 0 entries")
+			return nil
+		}
+
+		isMember := false
+	Entries:
+		for _, group := range srg.Entries {
+			for _, member := range group.GetAttributeValues(ls.GroupMemberUID) {
+				if (ls.UserUID == "dn" && member == sr.Entries[0].DN) || member == uid {
+					isMember = true
+					break Entries
+				}
+			}
+		}
+
+		if !isMember {
+			log.Error("LDAP group membership test failed")
+			return nil
+		}
+	}
+
 	if isAttributeSSHPublicKeySet {
 		sshPublicKey = sr.Entries[0].GetAttributeValues(ls.AttributeSSHPublicKey)
 	}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 3a4bf8173f..d88ccd6c3d 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2098,6 +2098,11 @@ auths.filter = User Filter
 auths.admin_filter = Admin Filter
 auths.restricted_filter = Restricted Filter
 auths.restricted_filter_helper = Leave empty to not set any users as restricted. Use an asterisk ('*') to set all users that do not match Admin Filter as restricted.
+auths.verify_group_membership = Verify group membership in LDAP
+auths.group_search_base = Group Search Base DN
+auths.valid_groups_filter = Valid Groups Filter
+auths.group_attribute_list_users = Group Attribute Containing List Of Users
+auths.user_attribute_in_group = User Attribute Listed In Group
 auths.ms_ad_sa = MS AD Search Attributes
 auths.smtp_auth = SMTP Authentication Type
 auths.smtphost = SMTP Host
diff --git a/routers/admin/auths.go b/routers/admin/auths.go
index a4fd5290b7..98f6e25b1f 100644
--- a/routers/admin/auths.go
+++ b/routers/admin/auths.go
@@ -129,6 +129,11 @@ func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig {
 			AttributeSSHPublicKey: form.AttributeSSHPublicKey,
 			SearchPageSize:        pageSize,
 			Filter:                form.Filter,
+			GroupsEnabled:         form.GroupsEnabled,
+			GroupDN:               form.GroupDN,
+			GroupFilter:           form.GroupFilter,
+			GroupMemberUID:        form.GroupMemberUID,
+			UserUID:               form.UserUID,
 			AdminFilter:           form.AdminFilter,
 			RestrictedFilter:      form.RestrictedFilter,
 			AllowDeactivateAll:    form.AllowDeactivateAll,
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 15ab2b227b..e6c5cf2578 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -99,6 +99,31 @@
 					    <label for="attribute_ssh_public_key">{{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}</label>
 					    <input id="attribute_ssh_public_key" name="attribute_ssh_public_key" value="{{$cfg.AttributeSSHPublicKey}}" placeholder="e.g. SshPublicKey">
 					</div>
+					<div class="inline field">
+						<div class="ui checkbox">
+							<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label>
+							<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if $cfg.GroupsEnabled}}checked{{end}}>
+						</div>
+					</div>
+					<div id="groups_enabled_change">
+						<div class="field">
+							<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label>
+							<input id="group_dn" name="group_dn" value="{{$cfg.GroupDN}}" placeholder="e.g. ou=group,dc=mydomain,dc=com">
+						</div>
+						<div class="field">
+							<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label>
+							<input id="group_filter" name="group_filter" value="{{$cfg.GroupFilter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))">
+						</div>
+						<div class="field">
+							<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label>
+							<input id="group_member_uid" name="group_member_uid" value="{{$cfg.GroupMemberUID}}" placeholder="e.g. memberUid">
+						</div>
+						<div class="field">
+							<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label>
+							<input id="user_uid" name="user_uid" value="{{$cfg.UserUID}}" placeholder="e.g. uid">
+						</div>
+						<br/>
+					</div>
 					{{if .Source.IsLDAP}}
 						<div class="inline field">
 							<div class="ui checkbox">
diff --git a/templates/admin/auth/source/ldap.tmpl b/templates/admin/auth/source/ldap.tmpl
index f5806c829c..651877b1f7 100644
--- a/templates/admin/auth/source/ldap.tmpl
+++ b/templates/admin/auth/source/ldap.tmpl
@@ -71,6 +71,31 @@
 	    <label for="attribute_ssh_public_key">{{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}</label>
 	    <input id="attribute_ssh_public_key" name="attribute_ssh_public_key" value="{{.attribute_ssh_public_key}}" placeholder="e.g. SshPublicKey">
 	</div>
+	<div class="inline field">
+		<div class="ui checkbox">
+			<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label>
+			<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if .groups_enabled}}checked{{end}}>
+		</div>
+	</div>
+	<div id="groups_enabled_change">
+		<div class="field">
+			<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label>
+			<input id="group_dn" name="group_dn" value="{{.group_dn}}" placeholder="e.g. ou=group,dc=mydomain,dc=com">
+		</div>
+		<div class="field">
+			<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label>
+			<input id="group_filter" name="group_filter" value="{{.group_filter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))">
+		</div>
+		<div class="field">
+			<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label>
+			<input id="group_member_uid" name="group_member_uid" value="{{.group_member_uid}}" placeholder="e.g. memberUid">
+		</div>
+		<div class="field">
+			<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label>
+			<input id="user_uid" name="user_uid" value="{{.user_uid}}" placeholder="e.g. uid">
+		</div>
+		<br/>
+	</div>
 	<div class="ldap inline field {{if not (eq .type 2)}}hide{{end}}">
 		<div class="ui checkbox">
 			<label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label>
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 91b6936a50..98a1033955 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -1795,6 +1795,14 @@ function initAdmin() {
     }
   }
 
+  function onVerifyGroupMembershipChange() {
+    if ($('#groups_enabled').is(':checked')) {
+      $('#groups_enabled_change').show();
+    } else {
+      $('#groups_enabled_change').hide();
+    }
+  }
+
   // New authentication
   if ($('.admin.new.authentication').length > 0) {
     $('#auth_type').on('change', function () {
@@ -1835,6 +1843,7 @@ function initAdmin() {
       }
       if (authType === '2' || authType === '5') {
         onSecurityProtocolChange();
+        onVerifyGroupMembershipChange();
       }
       if (authType === '2') {
         onUsePagedSearchChange();
@@ -1845,12 +1854,15 @@ function initAdmin() {
     $('#use_paged_search').on('change', onUsePagedSearchChange);
     $('#oauth2_provider').on('change', onOAuth2Change);
     $('#oauth2_use_custom_url').on('change', onOAuth2UseCustomURLChange);
+    $('#groups_enabled').on('change', onVerifyGroupMembershipChange);
   }
   // Edit authentication
   if ($('.admin.edit.authentication').length > 0) {
     const authType = $('#auth_type').val();
     if (authType === '2' || authType === '5') {
       $('#security_protocol').on('change', onSecurityProtocolChange);
+      $('#groups_enabled').on('change', onVerifyGroupMembershipChange);
+      onVerifyGroupMembershipChange();
       if (authType === '2') {
         $('#use_paged_search').on('change', onUsePagedSearchChange);
       }