Keycloak に Duo Security の 2FA を追加した

# Background
Keycloak を LDAP 認証に繋げていましたが、 2FA したくなったのでやってみました。

# Goal
Keycloakのログイン画面で今まで通りの LDAP or kerberos 認証を行います。
認証に成功すると、DuoにRedirectされます。
Duoがスマホにpush通知を送ります。
approveすると、ログイン成功となりサービスの画面が開きます。



# Preparation: Keycloak を upgrade する
まず今回使う Keycloak Plugin はこちらです。

DuoUniversalKeycloakAuthenticator

https://github.com/instipod/DuoUniversalKeycloakAuthenticator

現時点では v1.0.7 が最新です。Keycloakは 23 が出てますが、22.0.1に対応してます。

https://github.com/instipod/DuoUniversalKeycloakAuthenticator/releases/tag/1.0.7

Keycloak 23.0.1 でも動いたんですが、設定画面が一部表示されなかったりしたので素直に Keycloak 22.0.1 で使いました。

Keycloakのupgradeが必要な人は前回の記事を見てください。WildFly版の Keycloak には（多分）非対応なので、素直に Quarkus な v22 まで上げましょう。v16 に未来はないです。

https://qiita.com/uturned0/items/e9256c48ccba6f588d79

Keycloak 23 に対応する PR も出てるので、Java 得意な人は build してみてください。私は苦手なので...

# Keycloak に DuoUniversalKeycloakAuthenticator を追加しよう
## Dockerfile
docker imageに DuoUniversalKeycloakAuthenticator を追加します。

まず、入れたい DuoUniversalKeycloakAuthenticator を ↓ からダウンロードしておきます。.jar ファイルが登録されています。maven の build いらず。Java 苦手民に優しい。

https://github.com/instipod/DuoUniversalKeycloakAuthenticator/releases

ダウンロードした .jar を Keycloak の image に追加します。
official doc では build stageを分けるように言っているのでその通りに従ってみました。

```rb:Dockerfile
FROM quay.io/keycloak/keycloak:22.0.1-2 as builder

WORKDIR /opt/keycloak
COPY DuoUniversalKeycloakAuthenticator-1.0.7_22.0.1wd.jar  /opt/keycloak/providers

RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:22.0.1-2
COPY --from=builder /opt/keycloak/ /opt/keycloak/

EXPOSE 8080
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start-dev"]
```

この部分でpluginを追加しています。特定のファイル名に rename とかは不要です。

```rb
COPY DuoUniversalKeycloakAuthenticator-1.0.7_22.0.1wd.jar  /opt/keycloak/providers
```

Quarkus版ではプラグインのフォルダが `/opt/keycloak/providers` に変わっています。ここに `.jar` を置いておくと、 keycloakを起動(`kc.sh start/start-dev`) した時に、まずこの plugin をinstallしてから動作を開始してくれます。便利。


## Duo Universal MFA Config の設定

Keycloakを起動したら admin console を開きます。あとは README の通りです。

https://github.com/instipod/DuoUniversalKeycloakAuthenticator

想定外だったのは、 Keycloak side bar の `Identity providers` を使わないことでした。plugin は専用の auth step として追加されてます。へ〜〜。

1. Side menu から Authentication を開く
    1. ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24737/17e31e07-2adc-b798-8612-79244012cd15.png)
1. `browser` をduplicateして、`Copy of Browser` を作ります。
1. `Copy of Browser` を開きます
1. `Add step` を押します
1. `Duo Universal MFA` を探して追加します。`Required` にします。
1. `Username Password Form` の下に移動します。
    1. これで通常のログイン画面が終わった後に実行されます。
1. Gear iconをクリックして設定画面を開きます。
    1. ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24737/5b2d634c-540d-6430-60fc-ce754066df74.png)
1. Duo の `WebSDK` で発行したコードを入力します。
    1. Duo の情報はこう mapping されます
        1. `Client ID` -> `Duo Integration Key`
        1. `Client secret` -> `Duo Secret Key`
        1. `API hostname` -> `Duo API Hostname`
        1. ![Screenshot 2023-12-15 at 16.14.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24737/73bc622d-ebb5-4213-f886-ea851943f3e0.png)
    1. テスト中は `Fail Safe` を on にした方がいいです。Duo 認証に失敗してもログインできます。Admin consoleに 2FA を追加して admin が入れなくなると、めんどいので・・・ :(
    1. `Client Overrides` は、Clientごとに Duo の key を変えたい時に使えそうです。（試してない）
        1. (Clientごとに Auth Flow を作ったほうがわかりやすいような気もするけど、それも大変だからこの機能があるんだろうな・・)
1. とある Keycloak clientにかける場合は、clientのAdvancedな設定から Authentication flowを `Copy of Browser` で override すれば試せます。
1. `side bar` > `Authentication` > `Copy of browser` > "..." なコンテキストぽいメニュー > `Bind flow` で `browser` に当てれば admin console含めて web form全部に適用されますけど、ミスるとログインできずなんの設定も変えれなくなります・・ :(


## Troubleshooting

うまく行かない時はまずそのサーバから Duo に push できるか試しましょう。これはpythonの例です。

```python
import duo_client
import json

# Duo Securityの設定
ikey = 'Client ID'
skey = 'your-secret-key'
host = 'api-xxxxxxxx.duosecurity.com'

username = 'your duo user id`

# Duo Securityのクライアントを作成
client = duo_client.Auth(ikey, skey, host)

# Duo SecurityのAPIに接続
response = client.check()

# レスポンスを表示
print(response)

# Device状況を確認
res = client.preauth(username)
print(json.dumps(res, indent=2))

# Push送信
res = client.auth('push', username=username, device='auto')
print(json.dumps(res, indent=2))
```

### Errors

#### Login request denied.
Duo WebSDK の IP ACL にかかると、`Login request denied.` エラーが出ます。Duo側の設定を見直しましょう。そのサーバが使う Public IP address を許可する必要があります。

```rb
{
  "result": "deny",
  "status": "deny",
  "status_msg": "Login request denied."
}
```

#### Read time out
`preauth()` は成功するのに `auth()` が成功しないという悪夢がありました。Keycloak log を見ると
Keycloak が duo exception で `read timeout` を吐いてました。Plugin のエラーハンドリングは効いていません。これはなんと、その環境がたまたま MTU の問題を抱えていて特定のサイズを超えるとパケットの送受信ができないからでした。まともにインターネットができなかったとは・・。これはハマった。。。そんなのを引くのは私くらいでしょう。


# 終わり
書いてみると簡単ですけど、それはまあ関係ない問題もありハマりにハマりました。どうか皆さん、気楽な 2FA ライフを送ってください。人柱は自分だけで十分だ！