[LDAP][dex] dexidp で LDAP シンプル・バインド・モード接続

前回の記事で、

AWS ALB の LDAP 認証連携の構築
https://qiita.com/batatch/items/1577d453944070c19365


dexidp による OIDC-LDAP変換で ALB 認証連携を実施したのですが、実は LDAPサーバとなる ADにはそのままでは連携できなかったので、dexidpに少し手入れが必要でした。

通常、Apache等で認証連携するときは、管理アカウントでバインドしてから、ユーザを検索/認証するという動きをするのですが、対象の ADでは、直接ユーザアカウントでバインドするというものでした。
そのため、Apacheなどの静的設定で利用されておらず、プログラムで独自に接続する形になっていました。

この対応についての記録になります。


## LDAP方式の違い

この LDAPの方式の違いがどういうものか分からず、調べてみたところ、静的な設定ファイルで 2種類の方式に対応している例がありました。

PostgreSQL 文書 / 20.10. LDAP認証
https://www.postgresql.jp/document/11/html/auth-ldap.html

このマニュアルでは、2種類の LDAP方式を以下のように説明しています。

1. シンプル・バインド・モード (simple bind mode)
1. サーチ+バインド・モード (search+bind mode)

後者が Apache等の設定でよく見る管理者アカウントで事前にバインドするもの、
前者が今回の ADのようにユーザアカウントで直接バインドするものです。

設定ファイルには、ldapbinddn/ldapbindpasswd でバインド用の管理者アカウントを設定すれば「サーチ+バインド・モード」、
ldapprefix/ldapsuffix を設定すれば「シンプル・バインド・モード」となるようです。

シンプル・バインド・モードでは、*prefix username suffix* という形でユーザアカウントでバインドするようになります。

これを参考に、dexidp の LDAPコネクタに設定項目を追加できるようにしてみます。


## dexidp へ LDAP シンプル・バインド・モードを追加

PostgreSQLを参考に、以下の形で設定できるようにします。
**bindDNPrefix**、**bindDNSuffix** というパラメータで、シンプル・バインド・モードの場合に、ユーザアカウントの前後に追加するラベルを指定します。

```/dex/config.yaml
issuer: https://oidc.example.com/dex
storage:
  type: sqlite3
  config:
    file: dex/dex.db
web:
  http: 0.0.0.0:5556
connectors:
- type: ldap
  name: OpenLDAP
  id: ldap
  config:
    host: ldap.example.com:389
    insecureNoSSL: true
#    bindDN: cn=admin,dc=example,dc=org
#    bindPW: admin
    bindDNPrefix: "cn="                           ★追加
    bindDNSuffix: ",ou=People,dc=example,dc=org"  ★追加
    usernamePrompt: User ID
    userSearch:
      baseDN: ou=People,dc=example,dc=com
      username: mail
      idAttr: DN
      emailAttr: mail
      nameAttr: cn
    groupSearch:
      baseDN: ou=Groups,dc=example,dc=com
      userAttr: DN
      groupAttr: member
      nameAttr: cn
staticClients:
- id: app-id
  redirectURIs:
  - 'https://app.example.com/oauth2/idpresponse'
  name: 'App'
  secret: ZXhhbXBsZS1hcHAtc2VjcmV0
```

ソースの変更内容は以下のとおり。

```
diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go
index aed7319..f1ddf01 100644
--- a/connector/ldap/ldap.go
+++ b/connector/ldap/ldap.go
@@ -31,6 +31,9 @@ import (
 //       rootCA: /etc/dex/ldap.ca
 //       bindDN: uid=seviceaccount,cn=users,dc=example,dc=com
 //       bindPW: password
+//       # for ldap simple bind mode.
+//       bindDNPrefix: uid=
+//       bindDNSuffix: ,cn=users,dc=example,dc=com
 //       userSearch:
 //         # Would translate to the query "(&(objectClass=person)(uid=<username>))"
 //         baseDN: cn=users,dc=example,dc=com
@@ -81,6 +84,11 @@ type Config struct {
        BindDN string `json:"bindDN"`
        BindPW string `json:"bindPW"`

+       // BindDNPrefix and BindDNSuffix for LDAP Simple bind mode.
+       // The connector uses BindDNPrefix username BindDNSuffix as bind username.
+       BindDNPrefix string `json:"bindDNPrefix"`
+       BindDNSuffix string `json:"bindDNSuffix"`
+
        // UsernamePrompt allows users to override the username attribute (displayed
        // in the username/password prompt). If unset, the handler will use
        // "Username".
@@ -249,7 +257,7 @@ func (c *Config) openConnector(logger log.Logger) (*ldapConnector, error) {
        if !ok {
                return nil, fmt.Errorf("groupSearch.Scope unknown value %q", c.GroupSearch.Scope)
        }
-       return &ldapConnector{*c, userSearchScope, groupSearchScope, tlsConfig, logger}, nil
+       return &ldapConnector{*c, userSearchScope, groupSearchScope, tlsConfig, logger, "", []byte{}}, nil
 }

 type ldapConnector struct {
@@ -261,6 +269,9 @@ type ldapConnector struct {
        tlsConfig *tls.Config

        logger log.Logger
+
+       uid  string
+       pass []byte
 }

 var (
@@ -296,12 +307,26 @@ func (c *ldapConnector) do(ctx context.Context, f func(c *ldap.Conn) error) erro
        }
        defer conn.Close()

+       var (
+               bindDN string
+               bindPW string
+       )
+       bindDN = c.BindDN
+       bindPW = c.BindPW
+
+       if c.BindDNPrefix != "" || c.BindDNSuffix != "" {
+               bindDN = fmt.Sprintf("%s%s%s", c.BindDNPrefix, c.uid, c.BindDNSuffix)
+               bindPW = string(c.pass)
+       }
+
+       c.logger.Debugf("ldap: - bindDN=<%s>", bindDN)
+
        // If bindDN and bindPW are empty this will default to an anonymous bind.
-       if err := conn.Bind(c.BindDN, c.BindPW); err != nil {
-               if c.BindDN == "" && c.BindPW == "" {
+       if err := conn.Bind(bindDN, bindPW); err != nil {
+               if bindDN == "" && bindPW == "" {
                        return fmt.Errorf("ldap: initial anonymous bind failed: %v", err)
                }
-               return fmt.Errorf("ldap: initial bind for user %q failed: %v", c.BindDN, err)
+               return fmt.Errorf("ldap: initial bind for user %q failed: %v", bindDN, err)
        }

        return f(conn)
@@ -414,6 +439,10 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
 }

 func (c *ldapConnector) Login(ctx context.Context, s connector.Scopes, username, password string) (ident connector.Identity, validPass bool, err error) {
+
+       c.uid = username
+       c.pass = []byte(password)
+
        // make this check to avoid unauthenticated bind to the LDAP server.
        if password == "" {
                return connector.Identity{}, false, nil
```

上記の差分を patch 適用して dockerビルドするには、前回の Dockerfileを以下のように変更します。

```Dockerfile
FROM golang:1.13-alpine AS build-env
RUN apk add --no-cache --update alpine-sdk
RUN mkdir -p /go/src/github.com/dexidp \
    && cd /go/src/github.com/dexidp \
    && git clone https://github.com/dexidp/dex.git -b v2.21.0 --depth 1
COPY ldap_simple_bind.patch /go/src/github.com/dexidp/dex                 ★追加
RUN cd /go/src/github.com/dexidp/dex && git apply ldap_simple_bind.patch  ★追加

RUN cd /go/src/github.com/dexidp/dex && make release-binary

FROM alpine:3.10
RUN apk add --update ca-certificates openssl sqlite
USER root
COPY --from=build-env /go/bin/dex /usr/local/bin/dex
COPY --from=build-env /go/src/github.com/dexidp/dex/web /web
VOLUME /dex
WORKDIR /
ENTRYPOINT ["dex"]
```


GitHubにもアップしました。
https://github.com/batatch/dex/tree/feature/ldap_simple_bind