LDAP Authentication in Golang with Bind and Search
Tags: go, ldap, authentication
Published: 2021-06-30 19:31:52

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.*](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)

> **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**):
```golang
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:
```golang
// 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:
```golang
// 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:
```golang
// 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:
```golang
// 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

```golang
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

```golang
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:
```golang
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](https://gist.github.com/StarBuckR/68663e7b16e2e7ae7adb2e7a9b86a545) I created.

Thank you for reading. I hope you found this article helpful.