[ALB][OIDC][LDAP][dex] AWS ALB の LDAP 認証連携の構築

親記事はこちら
AWS ALB で LDAP 認証連携に対応する
https://qiita.com/batatch/items/fbc405e04edba75325ef


ALB による LDAP 認証連携を以下のように構築します。
OIDC-LDAP 変換の dexidp はシンプルに EC2 上の docker で動かします。

![alb-oidc-component.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558772/3803007f-3e8f-be62-dfe6-243c84574f4f.png)

LDAP 連携のため、社内システムを想定していますが、親記事に書いたように dexidp は正規の証明書を設定した HTTPS でアクセスさせる必要があるため、アプリ用と dexidp 用で ALB を分けています。

なお、dexidp から LDAP 接続するため、AWS と社内ネットワーク等がつながっていることとします。

## dexidp を起動する

まずは、EC2 上で dexidp を立ち上げます。

以下のように Dockerfile と docker-compose.yml を用意して 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
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"]
```

**docker-compose.yml**

```
version: "3.3"
services:
  dexidp:
    image: dexidp
    build:
      context: .
    command: >
      serve /dex/config.yaml
    volumes:
      - /data/dexidp:/dex
    ports:
      - "5554:5554"
      - "5556:5556"
      - "5558:5558"
```

dexidp の実行環境は以下のようになっていて、設定ファイルとストレージの SQLite ファイルを外部ボリューム参照にしています。

```
/usr/local/bin/dex : 実行ファイル
/web/ : 認証画面のソース
/dex/ --> ホスト:/data/ へマウント
                    config.yaml : 設定ファイル
                    dex.db : ストレージファイル(SQLite)
```

設定ファイルは LDAP 接続用に connectors の type に ldap を指定します。(LDAP 設定は環境に合わせて要確認)

また、issuer、staticClients.redirectURIs はそれぞれの ALB に割り当てるドメインに合わせて設定します。

- issuer : dexidp の前段に置く ALB のドメイン(正規の HTTPS 必須)
- staticClients.redirectURIs : アプリの前段に置く ALB のドメイン

**/dex/config.yaml**

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

各種ファイルを用意したら、docker をビルドして起動します。

```
$ docker-compose build
$ docker-compose up
```

下記の URL へアクセスし、OIDC の接続情報が返ってきたら dexidp の起動完了です。
接続情報は ALB への認証連携の設定で利用します。

```
$ curl localhost:5556/dex/.well-known/openid-configuration
{
  "issuer": "https://oidc.example.com/dex",
  "authorization_endpoint": "https://oidc.example.com/dex/auth",
  "token_endpoint": "https://oidc.example.com/dex/token",
  "jwks_uri": "https://oidc.example.com/dex/keys",
  "userinfo_endpoint": "https://oidc.example.com/dex/userinfo",
  "response_types_supported": [
    "code"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "groups",
    "profile",
    "offline_access"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic"
  ],
  "claims_supported": [
    "aud",
    "email",
    "email_verified",
    "exp",
    "iat",
    "iss",
    "locale",
    "name",
    "sub"
  ]
}
```

## ターゲットグループを作成する

dexidp 向けのターゲットグループを作成します。

**AWS コンソール ＞ 
EC2 ＞ ターゲットグループ ＞ ターゲットグループの作成**

- ターゲットグループ名: **tg-oidc**
- ターゲットの種類: インスタンス
- プロトコル: HTTP
- ポート: **5556**
- VPC: ****
- ヘルスチェック プロトコル: HTTP
- ヘルスチェック パス: **/dex/.well-known/openid-configuration**

ターゲット ＞ 編集から、対象の EC2 インスタンスを登録します。
この時点ではヘルスチェックは行われないので、ステータスは **unused** となります。
後ほど、ターゲットグループを ALB へ割り当てるとヘルスチェックが実施され、ステータスが **healthy** になります。

ターゲットが **healthy** にならないときは、EC2 のセキュリティグループで TCP 5556 ポートへのインバウンドが許可されていない可能性があります。

アプリ側も同様にターゲットグループを登録しておきます。

## ALB を作成する

dexidp 用の ALB を作成します。

こちらはパブリックなドメインと正規の SSL 証明書が必要になるので、Route53 等でドメインの登録、ACM で証明書の用意などを済ませておきます。
ここでは、以下のドメインを **oidc.example.com** とします。

**AWS コンソール ＞ 
EC2 ＞ ロードバランサ― ＞ ロードバランサ―の作成 ＞ Application Load Balancer の作成**

- 名前: **alb-oidc**
- スキーム: 内部
- IPアドレスタイプ: ipv4
- リスナーの追加 ロードバランサ―のプロトコル: HTTPS
- リスナーの追加 ロードバランサ―のプロトコル: 443
- アベイラビリティゾーン VPC: ****

HTTPS のリスナーを登録するので、証明書のタイプ、名前、セキュリティポリシーの選択などが続きます。

ルーティングの設定で、先ほど作成したターゲットグループを割り当てます。

- ターゲットグループ: 既存のターゲットグループ
- 名前: **tg-oidc**
- ターゲットの種類: インスタンス
- プロトコル: HTTP
- ポート: **5556**
- VPC: ****
- ヘルスチェック プロトコル: HTTP
- ヘルスチェック パス: **/dex/.well-known/openid-configuration**

ALB を作成したら、詳細画面の「リスナー」タブにて転送ルールの設定を行います。
dexidp 用は以下のルールとなります。

- IF: パスが /dex または /dex/*
- THEN:
  1. 転送先: tg-app

ルールを設定したら、ALB の個別ドメイン経由で dexidp へアクセスできることを確認しておきます。
(今回は内部 ALB なので、VPC 内の EC2 などから確認します)

```
$ curl https://internal-alb-oidc-0000000000.ap-northeast-1.elb.amazonaws.com/dex/.well-known/openid-configuration
```

ALB 経由でのアクセスができたら、DNS(Route53等) にてパブリックドメイン→個別ドメインの CNAME レコードを登録し、パブリックドメイン指定で dexidp へアクセスできることを確認します。

```
oidc.example.com.  CNAME  internal-alb-oidc-0000000000.ap-northeast-1.elb.amazonaws.com
```

```
$ curl https://oidc.example.com/dex/.well-known/openid-configuration
```

## アプリ ALB へ認証設定を追加する

アプリ側 ALB のリスナーのルールへ認証設定を追加します。

アプリ側の ALB はあらかじめ用意されているとし、dexidp とは別ドメイン(**app.example.local**) を設定していることとします。
アプリのパスは **/app/*** とします。

リスナールールの設定を開いて以下のルールを設定します。設定値は dexidp の **/dex/.well-known/openid-configuration** の結果、および設定ファイル **config.yaml** を参考にします。

- IF: パスが /app または /app/*
- THEN:
  1. 認証: OIDC
     - 発行者: https://oidc.example.com/dex
     - 認証エンドポイント: https://oidc.example.com/dex/auth
     - トークンエンドポイント: https://oidc.example.com/dex/token
     - ユーザ情報エンドポイント: https://oidc.example.com/dex/userinfo
     - クライアントID: alb-id
     - クライアントのシークレット: ZXhhbXBsZS1hcHAtc2VjcmV0
     - セッション Cookie 名: AWSELBAuthSessionCookie ※任意
     - セッションタイムアウト: 60 ※任意
     - スコープ: openid+profile+email
     - 認証されていないリクエストのアクション: 認証
  2. 転送先: tg-app

ブラウザでアプリの URL へアクセスし、dexidp のログインフォーム画面へリダイレクトされることを確認します。フォームから LDAP 認証アカウントを指定してログインができたら認証連携の成功です。

```
https://app.example.local/app
```

## アプリで認証済のアカウント名を取得する

ALB の認証連携を経てアプリに届いたリクエストには以下のヘッダが設定されています。

- x-amzn-oidc-data
- x-amzn-oidc-identity
- x-amzn-oidc-accesstoken

**x-amzn-oidc-data** と **x-amzn-oidc-accesstoken** は JWT 形式のトークンとなっていて、"." で 3つのトークンに区切られます。

以下のサイトでデコードすることができます。

https://jwt.io/

デコードしてみると、3つのトークンはそれぞれ HEADER、PAYLOAD、SIGNATURE に分割され、前者の 2つは JSON 文字列ということが分かります。

HEADER はアルゴリズムなどエンコード方法に関する情報で、アカウントやメールアドレスなどの属性情報は 2つめの PAYLOAD に格納されています。

自分で JWT をデコードする場合は、トークンを "." で分割し、2つめの PAYLOAD のトークンを Base64 デコードすれば JSON 形式で取得できます。

```json
{
  "iss": "https://oidc.example.com/dex",
  "sub": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "aud": "alb-id",
  "exp": 9999999999,
  "iat": 9999999999,
  "at_hash": "XXXXXXXXXXXXXXXXXXXXXX",
  "email": "user001@example.com",
  "email_verified": true,
  "name": "user001"
}
```

PAYLOAD の JSON 項目は設定次第になりますが、今回の LDAP 認証の場合は **name** としてアカウント名を参照できます。

今回は ALB で作成・設定された JWT をそのまま信用して利用しますが、本来は SIGNATURE を評価して JWT 自体の信頼性を検証します。