LDAP Authentication in Golang with Bind and Search

starbuckr

Rıdvan Tülemen

Posted on June 30, 2021

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
Enter fullscreen mode Exit fullscreen mode

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=*)"
)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

You can also check out the gist I created.

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

💖 💪 🙅 🚩
starbuckr
Rıdvan Tülemen

Posted on June 30, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related