自主的20%るぅる

各々が自主的に好き勝手書くゆるふわ会社ブログ

Go言語で LDAP (AD) 認証してやろうやないか!

Go言語で LDAP 認証したい!

皆さん、 Go言語書いてますか!
Go言語は最近はやりのマイクロサービスにも向いている感がありますし、
一時の熱は冷めつつあるものの、なかなか使い勝手は良い言語ですよね。

今回は、 Go言語で LDAP の認証をしたい!という方向けに
Go言語で LDAP サーバーとの通信・認証を行う方法についてみていきましょう。

ちなみに実際に使う LDAP サーバーは Microsoft の Active Directory (以下 AD) です。

コード、ドン!

今回書いたコードがこちら。

package main

import (
    "fmt"
    "os"
    "log"
    ldapConnect "gopkg.in/ldap.v2"
    "errors"
)

type Ldap struct {
    Host            string
    BindDN          string
    BindUser        string
    BindPassword    string
}

type LdapResult struct {
    LoginName   string
    Name        string
    DN          string
}

// LDAP 設定作成
func CreateLdapConn(ldapHost string, ldapBaseDN string, ldapBindUser string, ldapBindPassword string) *Ldap {
    return &Ldap{
        Host: ldapHost,
        BindDN: ldapBaseDN,
        BindUser: ldapBindUser,
        BindPassword: ldapBindPassword,
    }
}

// LDAP 認証
func (ldap *Ldap) Login(username string, password string) (*LdapResult, error) {
    // LDAP サーバー接続
    l, err := ldapConnect.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, 389))
    if err != nil {
        log.Printf("Cannot connect server.\n%s", err.Error())
        return nil, err
    }
    defer l.Close()

    // LDAP バインドユーザーでの接続
    err = l.Bind(ldap.BindUser, ldap.BindPassword)

    if err != nil {
        log.Printf("Cannot connect with bind user\n%s", err.Error())
        return nil, err
    }

    // LDAP 検索設定
    SearchRequest := ldapConnect.NewSearchRequest(
        ldap.BindDN,
        ldapConnect.ScopeWholeSubtree, ldapConnect.NeverDerefAliases, 0, 0, false,
        fmt.Sprintf("(sAMAccountName=%s)", username),
        []string{"dn", "sAMAccountName", "displayName"},
        nil,
    )

    log.Printf("Start search user: %s", username)

    // LDAP 検索実行
    sr, err := l.Search(SearchRequest)
    if err != nil {
        log.Printf("Search failed.\n%s", err.Error())
        return nil, err
    }

    // 検索結果数確認 (1 件に特定できない場合は失敗)
    if len(sr.Entries) == 0 {
        log.Printf("Not found user: %s", username)
        return nil, fmt.Errorf("not found user: %s", username)
    } else if len(sr.Entries) > 1 {
        log.Printf("Too many found user: %s", username)
        return nil, fmt.Errorf("too many found user: %s", username)
    }

    entity := sr.Entries[0]

    // 認証ユーザーパスワード正誤確認
    err = l.Bind(entity.DN, password)
    if err != nil {
        log.Printf("Authorization failed user: %s", username)
        return nil, err
    }

    log.Printf("Authorization success user: %s", username)

  // sAMAccountName, displayName, DN を返却
    return &LdapResult{
        LoginName: entity.GetAttributeValue("sAMAccountName"),
        Name: entity.GetAttributeValue("displayName"),
        DN: entity.GetAttributeValue("dn"),
    }, nil
}

パッと見では分量多めに見えますが、やっている内容は簡単です。
内容を見ていきましょう。

内容解説!

冒頭部分

import 等々

package main

import (
    "fmt"
    "os"
    "log"
    ldapConnect "gopkg.in/ldap.v2"
    "errors"
)

type Ldap struct {
    Host            string
    BindDN          string
    BindUser        string
    BindPassword    string
}

type LdapResult struct {
    LoginName   string
    Name        string
    DN          string
}

ここはそのままですね。
今回、 LDAP サーバーへの接続には https://github.com/go-ldap/ldap を使用させていただいていますので、これをほかの必要なものと一緒に import。

後は Ldap が LDAP サーバーへの接続情報を保持する為の構造体。
LdapResult が LDAP サーバーへの問合せ結果を返すための構造体ですね。

CreateLdapConn func

LDAP 接続情報格納

// LDAP 設定作成
func CreateLdapConn(ldapHost string, ldapBaseDN string, ldapBindUser string, ldapBindPassword string) *Ldap {
    return &Ldap{
        Host: ldapHost,
        BindDN: ldapBaseDN,
        BindUser: ldapBindUser,
        BindPassword: ldapBindPassword,
    }
}

ここも、接続情報を格納した Ldap のオブジェクトを作成してポインタ返してるだけですね。
実際に接続を行う際には、このオブジェクトに格納されている情報を使用することになります。

Login func

ここから、実際に認証を行う関数に入っていきます。
この関数は Ldap オブジェクトのポインタをレシーバに取っていますので、
接続情報はここからとってくることになりますね。

では上から順に見ていきます

LDAP サーバー接続

    // LDAP サーバー接続
    l, err := ldapConnect.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, 389))
    if err != nil {
        log.Printf("Cannot connect server.\n%s", err.Error())
        return nil, err
    }
    defer l.Close()

import した ldap ライブラリの Dial を呼び出すことで LDAP サーバーへ接続できます。
Dial(ネットワーク string, アドレス:ポート番号 string) の形ですね。
最後にコネクションを切るのを忘れないように、 defer l.Close() も呼んでおきましょう。

LDAP バインドユーザーでの接続

    err = l.Bind(ldap.BindUser, ldap.BindPassword)

    if err != nil {
        log.Printf("Cannot connect with bind user\n%s", err.Error())
        return nil, err
    }

接続ができたら、 Bind でバインドユーザーでの接続を行います。
バインドユーザーとは、認証作業を行う時に LDAP へ予めログインさせておき、リクエストされたユーザー情報を取得するためのユーザーですね。

err が返ってこなければ接続成功です。

LDAP 検索設定

    SearchRequest := ldapConnect.NewSearchRequest(
        ldap.BindDN,
        ldapConnect.ScopeWholeSubtree, ldapConnect.NeverDerefAliases, 0, 0, false,
        fmt.Sprintf("(sAMAccountName=%s)", username),
        []string{"dn", "sAMAccountName", "displayName"},
        nil,
    )

    log.Printf("Start search user: %s", username)

NewSearchRequest を使って、LDAP 検索の設定を行います。
NewSearchRequest(バインドDN, 検索スコープ, 関節参照設定, サイズリミット, タイムリミット, 属性 ID のみ返却のフラグ, フィルター, 検索対象の文字列, 返却に含む値) ですね。大体見たらわかるんじゃないかなと思います。

重要なのはフィルターと検索対象の文字列ですね。
今回は AD を使うので、ユーザー名が格納されている sAMAccountName に対して、 username の値と一致するのものを取得してくるように設定しています。

あと、返却に含む値には基本的に必要な値を列挙すればよいですが、 dn は後で使用するので必ず含めてください。

LDAP 検索実行

    // LDAP 検索実行
    sr, err := l.Search(SearchRequest)
    if err != nil {
        log.Printf("Search failed.\n%s", err.Error())
        return nil, err
    }

l.Search(SearchRequest) のように、 Search に 先ほど作成した SearchRequest を呼ぶことで、実際に検索をかけることができます。
結果は sr に入りますね。

検索結果数確認 (1 件に特定できない場合は失敗)

    // 検索結果数確認 (1 件に特定できない場合は失敗)
    if len(sr.Entries) == 0 {
        log.Printf("Not found user: %s", username)
        return nil, fmt.Errorf("not found user: %s", username)
    } else if len(sr.Entries) > 1 {
        log.Printf("Too many found user: %s", username)
        return nil, fmt.Errorf("too many found user: %s", username)
    }

    entity := sr.Entries[0]

結果の .Entries に結果が入っているので、スライスの長さを確認してみましょう。
0 件ならマッチ無しでユーザー名が誤っている可能性がありますね。
また、逆に 2 件以上ならユーザーが特定できない条件で検索してしまっているので、これも失敗です。
1 件だけであればユーザーが特定できたので、これを entity 変数へ入れて次へ進みます。

認証ユーザーパスワード正誤確認

    // 認証ユーザーパスワード正誤確認
    err = l.Bind(entity.DN, password)
    if err != nil {
        log.Printf("Authorization failed user: %s", username)
        return nil, err
    }

    log.Printf("Authorization success user: %s", username)

ここで、先ほど取得したユーザーのパスワードが、ユーザーが送ってきたパスワードと一致しているかを確認します。

確認の方法は簡単で、特定できたユーザーとパスワードを LDAP サーバーへ渡して、ログインができるかどうかを確認します。
err に何かが返ってきたら失敗でパスワード間違い、 nil ならログイン成功なのでパスワードが合っているということになりますね!

sAMAccountName, displayName, DN を返却

  // sAMAccountName, displayName, DN を返却
    return &LdapResult{
        LoginName: entity.GetAttributeValue("sAMAccountName"),
        Name: entity.GetAttributeValue("displayName"),
        DN: entity.GetAttributeValue("dn"),
    }, nil

最後に、取得できた情報を LdapResult に格納して呼び出し元に返してあげましょう。

終わりに

パッと見複雑そうに見えますが、結局やっていることは

  • LDAP サーバーへ認証用ユーザーで接続
  • 渡された情報からユーザーを検索
  • 見つかったユーザーでログインを試みて、ログイン出来たら OK !

というだけのことですね。

これで LDAP サーバーでの認証をマイクロサービスとして切り分けられそうですね!
Go言語、ぜひ活用してくださいね。

Let’s share this article!

{ 関連記事 }

{ この記事を書いた人 }

takato_ezaki
記事一覧