Autheliaでローカル開発用のOIDC環境を簡単に構築してみた

ローカル開発環境で簡単に扱えるOIDCのIdP(Identity Provider)として、[Authelia](https://www.authelia.com/)がなかなか良かったので簡単に紹介したいと思います。

## 背景

ある日、私が携わっているプロダクトにOIDCによるログイン機能を追加することになりました。
このプロダクトはチームメンバーが各自のローカル環境で開発しているものなので、IdPについても実際のIdPに各自のテストクライアントを登録したりせずに、**ローカルで簡単に構築**できたらいいなと思いました。
（もちろん、検証環境は実際に使用するIdPを使います）

そこで、ローカルに構築して使えそうなIdPのプロダクトをいくつか探してみたところ、Autheliaが手頃でよかった、というお話になります。

## Autheliaとは

[Authelia](https://www.authelia.com/)は、多要素認証やSSO（シングルサインオン）を実現するための、オープンソースのIAM（ID・アクセス管理）サーバーです。

主な特徴は以下のとおりです。

- 軽量・高速
    - Go製で、トップページの謳い文句によると、圧縮されたコンテナサイズは20MB未満、通常利用時のメモリ使用量は30MB未満だそうです
- SSO（シングルサインオン）機能
- 多要素認証（ワンタイムパスワード、モバイルプッシュ通知、WebAuthnなど）
- 柔軟なアクセスコントロールポリシー
    - 「このURLは2要素認証必須」とか
- 基本的には認証リバースプロキシとして動く
    - SSOでよくある方式
- OIDC IdPの機能も持っている
    - ただし、v4.38（2024年8月）の時点では**ベータ扱い**です
    - [OIDC実装に関するロードマップ](https://www.authelia.com/roadmap/active/openid-connect/)
- **全ての設定を設定ファイル（YAML）で行う**

最後の「全ての設定を設定ファイルで行う」というのが今回の要件に特にマッチしました。

Autheliaは、接続するクライアントの情報なども全て設定ファイルに記述する方式となっています。
通常の運用だと管理画面などから設定できたほうがよいのかもしれませんが、今回のローカル開発という状況では、`docker compose up -d`したら全て設定済みのIdPが動いてほしいので、むしろ嬉しい仕様でした。あらかじめ開発用に設定済みのファイルを用意しておけばいいですからね。

また、OIDCのIdP機能についてはv4.38（2024年8月）の時点ではベータ扱いとなっていましたが、今回のローカル開発に使用してみた程度では特に問題なさそうでした。

もちろん、プロダクション環境でAutheliaをIdPとして運用するようなことは、ベータの間は避けたほうがよいでしょうし、また、検証環境などでは本番と同じIdPを使用したほうがよいでしょう。

## Authelia以外の選択肢

Authelia以外の選択肢としては、以下のようなものを検討しました。

- [Keycloak](https://www.keycloak.org/)
    - この手のIAMプロダクトの中ではかなり有名で鉄板な感じがするやつです
    - 今回の要件に対してオーバースペックすぎるのと、Web画面上でセットアップを進めなければいけないので見送りました
- [ZITADEL](https://zitadel.com/)（クラウドサービスもありますが、セルフホストできます）
    - 今回の調査で初めて知ったのですが、Keycloakと対比した記事がよく出てくるぐらいすごそうなIAMプロダクトです
    - Keycloakと同じ理由で見送りましたが、個人的にとても興味を惹かれました
- [dex](https://dexidp.io/)
    - 複数の異なる認証ソースを統合するポータルというかプロキシのようなプロダクトです
    - dex自身が認証ソースを持つわけではなく、上流のIdPに委ねる形なので、今回の用途には合いませんでした
        - LDAPをソースにすれば使えそうですが、それはそれで面倒
- [node-oidc-server](https://github.com/panva/node-oidc-provider) ※ライブラリ
    - Node.jsでOIDCサーバーを構築するためのライブラリです（単体で起動して使うようなプロダクトではないです）
    - いいプロダクトが見つからなければ、最終手段として、これを使って簡単な実装をするのもありかなと考えていました

## Autheliaのセットアップ

前置きが長くなりましたが、早速Autheliaのセットアップをしてみたいと思います。
今回はDockerを利用してサービスを立ち上げます。

今回使用したバージョンは次のとおりです。

- Authelia 4.38.9

全体のファイル構成は次のとおりです。

```
.
├── authelia
│   ├── config                   ← 設定ファイル置き場
│   │   ├── configuration.yml    ← 設定ファイル
│   │   └── users_database.yml   ← ユーザー定義ファイル
│   └── data                     ← データ置き場
│       ├── db.sqlite3           ← データベース(今回はSQLite)
│       └── notification.txt     ← 実際の通知メールの代わりに保存されるテキスト
└── compose.yaml
```

### Docker (compose.yaml)

`compose.yaml`は次のとおりです。
ボリュームの部分は設定ファイル置き場とデータファイル置き場になります。
データ置き場はディレクトリだけ用意すれば、Autheliaが勝手にファイルを置きます。

Autheliaに`http://127.0.0.1:9091`でアクセスできるようにする想定です。

```yaml:compose.yaml
name: authelia-sandbox

services:
  authelia:
    image: authelia/authelia:4.38.9
    # restart: unless-stopped
    ports:
      - 9091:9091
    volumes:
      - ./authelia/config:/config:ro
      - ./authelia/data:/data
```

### Autheliaの設定

Autheliaの設定ファイルは、公式がコメント付きのテンプレートファイルを用意してくれているので、これをコピーして書き換えていきます。

https://github.com/authelia/authelia/blob/v4.38.9/config.template.yml

コメント付きで1400行前後と、なかなかボリュームのある設定ファイルとなっています。

### ローカルOIDCのための設定

今回の目的であるローカル環境用のOIDCとして最低限の設定値を載せておきます。

テンプレートのコメントを全て残したまま載せると膨大な量になってしまうので必要な項目のみに絞っています（それでも150行弱）。
各項目を丁寧に説明していると長くなってしまうので、項目名から察してもらうか[公式マニュアル](https://www.authelia.com/configuration/prologue/introduction/)を参照しながら設定してください。
私もテンプレートのコメントと公式マニュアルを交互に見ながら設定しました。

途中で出てくる秘密鍵の生成やクライアントシークレットのハッシュ値の生成については、生成コマンドを後で紹介します。
それ以外のキーやシークレットは平文で設定します（文字数や文字種に制限がある場合あり）。

ファイル名は`configuration.yml`とします。

```yaml:authelia/config/configuration.yml
theme: 'light'
server:
  address: 'tcp://:9091/'
log:
  level: 'debug'
telemetry:
  metrics:
    enabled: false
# TOTPの設定
totp:
  disable: false
  # TOTPのissuer名
  issuer: 'localhost'
# WebAuthnの設定
webauthn:
  disable: false
  # WebAuthnの表示名
  display_name: 'Authelia Sandbox'
identity_validation:
  reset_password:
    # パスワードリセットに使用するJWTのシークレット
    jwt_secret: 'authelia_sandbox_secret'
# 認証バックエンドの設定
authentication_backend:
  password_reset:
    disable: false
  # ファイルベースのユーザーデータベースの設定（他にはLDAPが使えます）
  file:
    # ユーザーを定義するYAMLファイル
    path: '/config/users_database.yml'
    watch: false
    search:
      email: false
      case_insensitive: false
    # パスワードのアルゴリズム
    password:
      algorithm: 'argon2'
      argon2:
        variant: 'argon2id'
        iterations: 3
        memory: 65536
        parallelism: 4
        key_length: 32
        salt_length: 16
# パスワードポリシー
password_policy:
  standard:
    enabled: false
    min_length: 8
    max_length: 0
    require_uppercase: true
    require_lowercase: true
    require_number: true
    require_special: true
  zxcvbn:
    enabled: false
    min_score: 3
privacy_policy:
  enabled: false
  require_user_acceptance: false
  policy_url: ''
# アクセスコントロールの設定（今回はあまり関係ない気がする）
access_control:
  default_policy: 'deny'
  rules:
    - domain:
        - '127.0.0.1'
        - 'localhost'
        - '*.localhost'
      policy: 'one_factor'
# AutheliaのセッションとCookieの設定
# セッションストレージはインメモリのほかにRedisが使える
session:
  # セッションデータのシークレット
  secret: 'authelia_sandbox_secret'
  # ここは立ち上げる環境に合わせてドメインとURLを設定する
  # 本当は localhost としたかったが、ドットが含まれている必要があるようなので、127.0.0.1 とした
  # もちろん、hosts 等を書き換えて適当なドメインを振ってもよい
  cookies:
    - name: 'authelia_sandbox_session'
      domain: '127.0.0.1'
      authelia_url: 'https://127.0.0.1:9091'
  name: 'authelia_sandbox_session'
  same_site: 'lax'
  inactivity: '5m'
  expiration: '1h'
  remember_me: '1M'
# Autheliaの管理するデータのストレージ(SQLite, MySQL, PostgreSQL)
storage:
  encryption_key: 'authelia_sandbox_encryption_key'
  # SQLite
  local:
    path: '/data/db.sqlite3'
# 通知メールの設定
# SMTPの他に、テキストファイルに保存することができる
notifier:
  disable_startup_check: false
  filesystem:
    # メール送信の代わりに保存されるテキストファイル
    filename: '/data/notification.txt'
identity_providers:
  oidc:
    # HMACシークレット
    hmac_secret: 'this_is_a_secret_abc123abc123abc'
    # JWKs
    jwks:
      - key_id: 'authelia_sandbox'
        algorithm: 'RS256'
        use: 'sig'
        # JWTの署名に使用する秘密鍵
        key: |
          -----BEGIN PRIVATE KEY-----
          MIIJQQIBADANB...Eb0X26yBScK7gK
          -----END PRIVATE KEY-----
    # クライアントアプリ(OIDC RP)の設定
    # grant_type等はお好みで設定してください
    # ここでは単純なOIDCログインのために、id_tokenのみのimplicitフローを許可しました
    clients:
      - client_id: 'authelia_sandbox_client'
        client_name: 'Authelia Sandbox Client'
        # シークレットは平文ではなくハッシュ値で設定する必要がある
        # `authelia crypto hash generate pbkdf2` で生成できる
        client_secret: '$pbkdf2-sha512$310000$c8p78n7...'
        public: false
        # コールバックのURL
        # ここでは auth2c というコマンド用のコールバックURLを設定している
        redirect_uris:
          - 'http://localhost:9876/callback'
        scopes:
          - 'openid'
          - 'groups'
          - 'email'
          - 'profile'
        grant_types:
          - 'authorization_code'
          - 'implicit'
        response_types:
          - 'code'
          - 'id_token'
          - 'code id_token'
        authorization_policy: 'one_factor'
        require_pkce: true
        pkce_challenge_method: 'S256'
```

#### JWKsで使用する秘密鍵の生成

`identity_providers.oidc.jwks.*.key`に指定する秘密鍵は`openssl genrsa`コマンドで生成します。

```sh:生成例
openssl genrsa -out authelia/secrets/oidc_key.pem 4096
```

#### クライアントシークレットのハッシュ値の生成

`identity_providers.oidc.clients.*.client_secret`に指定する値は、autheliaに内蔵されているコマンドで生成できます。

```sh
authelia crypto hash generate pbkdf2

# Dockerの場合はこんな感じ
docker run -it --rm authelia/authelia:4.38.9 authelia crypto hash generate pbkdf2
```

実行すると対話的に値の入力を促されるので、クライアントシークレットの平文を入力します。
確認の入力を促されるので再度入力すると、ハッシュ値が得られます。

```sh:実行例
> authelia crypto hash generate pbkdf2
Enter Password:
Confirm Password:

Digest: $pbkdf2-sha512$310000$8uMYYde8u...
```

### ユーザーデータベースファイルの作成

Autheliaが管理するユーザーデータ（要するにログインユーザー）を用意します。

今回は開発用に固定のユーザーを何人か用意できればよいので、YAMLファイルで静的に管理することにします（LDAPを利用することも出来ます）。

フォーマットは次のような感じです（[公式マニュアルはこちら](https://www.authelia.com/reference/guides/passwords/#yaml-format)）。

```yaml:authelia/config/users_database.yml
users:
  ユーザーID:
    disabled: 無効化するかどうか
    displayname: 表示名
    password: パスワードのハッシュ値
    email: メールアドレス
    groups:
      - 所属グループ
      - ...
  
```

```yaml:サンプル
users:
  sandbox:
    disabled: false
    displayname: "user1"
    password: "$argon2id$v=19$m=65536,t=3,p=4$hsJou2WZ..."
    email: "user1@example.com"
    groups:
      - "sandbox"
```

#### ユーザーパスワードの生成

パスワードの部分はクライアントシークレットの時と同じように、autheliaのコマンドで生成できます。
アルゴリズムの部分は設定ファイルで指定したアルゴリズムと合わせてください。

```sh
authelia crypto hash generate argon2

# Dockerの場合はこんな感じ
docker run -it --rm authelia/authelia:4.38.9 authelia crypto hash generate argon2
```

### Autheliaの起動

ここまで準備できましたら、後はDockerで起動するだけです。

```sh
docker compose up -d
```

### 動作確認

無事に起動できたなら、早速動作確認を行ってみましょう。
動作確認には[oauth2c](https://github.com/cloudentity/oauth2c)というCLIツールが便利です。
macOSの場合は `brew install cloudentity/tap/oauth2c` でインストールできます。

次のコマンドを実行すると、ブラウザが開いてAutheliaのログイン画面が表示されるはずです。

```sh
oauth2c http://127.0.0.1:9091 \
  --client-id authelia_sandbox_client \
  --response-types id_token \
  --response-mode form_post \
  --grant-type implicit \
  --scopes openid,email
```

ユーザーデータベースファイルに定義したユーザーでログインしてOIDCのリクエストに同意すると、コンソールにID Tokenとフローの途中経過のステータスが表示されているはずです。

## おわりに

Autheliaを使うことで手軽に開発用のIdPを立ち上げることができ、無事にOIDCログイン機能も完成しました🎉

最初は設定ファイルが膨大で若干怯みましたが、全体像が掴めるようになってくると、わりとすんなり設定できるようになります。
起動も速いので、試行錯誤もそれほど苦にはなりませんでした。

また、oauth2cコマンドはOAuth2/OIDCの動作確認にとても便利でした。
わざわざサンプルアプリ等を作らなくてもコマンド一発で動作確認できるのは良いですね。

最近はOIDC連携によるログインも多くなってきましたし、今後の開発でも活用していきたいと思っています。