LDAP Authentication in Golang with Bind and Search
Rıdvan Tülemen
Posted on June 30, 2021
If you are familiar with the Windows Active Directory or Samba, you may have already heard about LDAP. But if you didn't, here is the description in Wikipedia.
Lightweight Directory Access Protocol (LDAP) is an open, vendor-neutral, industry standard application protocol for accessing and maintaining distributed directory information services over an Internet Protocol (IP) network.
Creating our project
First of all, we need to download the library:
go get github.com/go-ldap/ldap
Variables
Now, we can start using the library. Firstly, we are creating the variables to use them later(If you are going to use anonymous bind, you only need Filter):
const (
BindUsername = "user@example.com"
BindPassword = "password"
FQDN = "DC.example.com"
BaseDN = "cn=Configuration,dc=example,dc=com"
Filter = "(objectClass=*)"
)
Connect
To connect to LDAP, we can use ldap.DialURL()
func. Followed function is solely created to connect to an LDAP server:
// Ldap Connection without TLS
func Connect() (*ldap.Conn, error) {
// You can also use IP instead of FQDN
l, err := ldap.DialURL(fmt.Sprintf("ldap://%s:389", FQDN))
if err != nil {
return nil, err
}
return l, nil
}
TLS Connect
If you need TLS Connection, you can use the function below:
// Ldap Connection with TLS
func ConnectTLS() (*ldap.Conn, error) {
// You can also use IP instead of FQDN
l, err := ldap.DialURL(fmt.Sprintf("ldaps://%s:636", FQDN))
if err != nil {
return nil, err
}
return l, nil
}
Anonymous Bind and Search
If you want anonymous bind in your project, you can use this function:
// Anonymous Bind and Search
func AnonymousBindAndSearch(l *ldap.Conn) (*ldap.SearchResult, error) {
l.UnauthenticatedBind("")
anonReq := ldap.NewSearchRequest(
"",
ldap.ScopeBaseObject, // you can also use ldap.ScopeWholeSubtree
ldap.NeverDerefAliases,
0,
0,
false,
Filter,
[]string{},
nil,
)
result, err := l.Search(anonReq)
if err != nil {
return nil, fmt.Errorf("Anonymous Bind Search Error: %s", err)
}
if len(result.Entries) > 0 {
result.Entries[0].Print()
return result, nil
} else {
return nil, fmt.Errorf("Couldn't fetch anonymous bind search entries")
}
}
Bind and Search
If you prefer normal binding instead:
// Normal Bind and Search
func BindAndSearch(l *ldap.Conn) (*ldap.SearchResult, error) {
l.Bind(BindUsername, BindPassword)
searchReq := ldap.NewSearchRequest(
BaseDN,
ldap.ScopeBaseObject, // you can also use ldap.ScopeWholeSubtree
ldap.NeverDerefAliases,
0,
0,
false,
Filter,
[]string{},
nil,
)
result, err := l.Search(searchReq)
if err != nil {
return nil, fmt.Errorf("Search Error: %s", err)
}
if len(result.Entries) > 0 {
return result, nil
} else {
return nil, fmt.Errorf("Couldn't fetch search entries")
}
}
Main Func
Last of all, we need to create a main function to use these functions:
Bind and Search with TLS Connection
func main() {
// TLS Connection
l, err := ConnectTLS()
if err != nil {
log.Fatal(err)
}
defer l.Close()
// Normal Bind and Search
result, err = BindAndSearch(l)
if err != nil {
log.Fatal(err)
}
result.Entries[0].Print()
}
Anonymous Bind and Search with Non-TLS Connection
func main() {
// Non-TLS Connection
l, err := Connect()
if err != nil {
log.Fatal(err)
}
defer l.Close()
// Anonymous Bind and Search
result, err := AnonymousBindAndSearch(l)
if err != nil {
log.Fatal(err)
}
result.Entries[0].Print()
}
Conclusion
After these functions, I believe you can start using LDAP authentication, bind and search. At the end, the code should look like this:
package main
import (
"fmt"
"log"
"github.com/go-ldap/ldap/v3"
)
const (
BindUsername = "user@example.com"
BindPassword = "password"
FQDN = "DC.example.com"
BaseDN = "cn=Configuration,dc=example,dc=com"
Filter = "(objectClass=*)"
)
func main() {
// TLS Connection
l, err := ConnectTLS()
if err != nil {
log.Fatal(err)
}
defer l.Close()
// Non-TLS Connection
//l, err := Connect()
//if err != nil {
// log.Fatal(err)
//}
//defer l.Close()
// Anonymous Bind and Search
result, err := AnonymousBindAndSearch(l)
if err != nil {
log.Fatal(err)
}
result.Entries[0].Print()
// Normal Bind and Search
result, err = BindAndSearch(l)
if err != nil {
log.Fatal(err)
}
result.Entries[0].Print()
}
// Ldap Connection with TLS
func ConnectTLS() (*ldap.Conn, error) {
// You can also use IP instead of FQDN
l, err := ldap.DialURL(fmt.Sprintf("ldaps://%s:636", FQDN))
if err != nil {
return nil, err
}
return l, nil
}
// Ldap Connection without TLS
func Connect() (*ldap.Conn, error) {
// You can also use IP instead of FQDN
l, err := ldap.DialURL(fmt.Sprintf("ldap://%s:389", FQDN))
if err != nil {
return nil, err
}
return l, nil
}
// Anonymous Bind and Search
func AnonymousBindAndSearch(l *ldap.Conn) (*ldap.SearchResult, error) {
l.UnauthenticatedBind("")
anonReq := ldap.NewSearchRequest(
"",
ldap.ScopeBaseObject, // you can also use ldap.ScopeWholeSubtree
ldap.NeverDerefAliases,
0,
0,
false,
Filter,
[]string{},
nil,
)
result, err := l.Search(anonReq)
if err != nil {
return nil, fmt.Errorf("Anonymous Bind Search Error: %s", err)
}
if len(result.Entries) > 0 {
result.Entries[0].Print()
return result, nil
} else {
return nil, fmt.Errorf("Couldn't fetch anonymous bind search entries")
}
}
// Normal Bind and Search
func BindAndSearch(l *ldap.Conn) (*ldap.SearchResult, error) {
l.Bind(BindUsername, BindPassword)
searchReq := ldap.NewSearchRequest(
BaseDN,
ldap.ScopeBaseObject, // you can also use ldap.ScopeWholeSubtree
ldap.NeverDerefAliases,
0,
0,
false,
Filter,
[]string{},
nil,
)
result, err := l.Search(searchReq)
if err != nil {
return nil, fmt.Errorf("Search Error: %s", err)
}
if len(result.Entries) > 0 {
return result, nil
} else {
return nil, fmt.Errorf("Couldn't fetch search entries")
}
}
You can also check out the gist I created.
Thank you for reading. I hope you found this article helpful.
Posted on June 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.