LDAP Authentication in Golang with Bind and Search

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 = "[email protected]"
    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 = "[email protected]"
    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.

17