KeyCloakを使ってSAMLのIdPサーバーをローカルで立ち上げる方法

# はじめに
こんにちは。グロービスにてGLOPLA LMSのバックエンド開発を担当している山下です。

GLOPLA LMSはBtoBのサービスになりますが、規模の大きなお客様の場合は社内認証基盤を使っているケースも多く、そういった場合にはそれらを使ったSSO(シングルサインオン)をサポートしています。

このSSOの挙動確認を行う方法の一つとしてローカルにSAMLのIdPサーバーを立てるという手段がありますが、これに関するまとまった情報が意外と少なかったため、一連の手順について記事にまとめていきたいと思います。


# 0. 前提条件
- SAML2.0に対応したSP(ServiceProvider) は別途立ち上げる
- アカウント照合のキーとしてメールアドレスを使っている

実際のユースケースとしては以下の流れでログインが行われる想定です。

1. エンドユーザーが弊社のログイン画面でログインボタンを押す
2. 弊社がSPとなり、SAMLRequestのアサーションとともにIdPへリダイレクトする 
3. エンドユーザーは、IdPのログイン画面にてログインを行う
4. IdPからSAML ResponseのアサーションとともにSPへリダイレクトされる
5. SP, IdPで期待されるアサーションが連携できていればログインが成功する

SAMLやアサーションについての詳細な解説はこちらの記事が参考になりそうです。
https://qiita.com/sibakenY/items/113ef1439aaaa1e86184

では、ここから先は構築手順について記載していきます。


# 1. KeyCloakを立ち上げる

サーバーをIdPとして機能させるためのOSSとして **KeyCloak** というものがあります。

[https://www.keycloak.org](https://www.keycloak.org/)

今回はJBossが配布しているdockerイメージを使います。

以下の `docker-compose.yml` で立ち上げますが、ポイントとしては環境変数でadminを作成済みの状態で立ち上げていることと、`keycloak_db_data`というボリュームを作って永続化しているところですね。

これをやるといつものように `docker compose up`だけで立ち上がるのですが、 立ち上げ -> 落として再起動 した際に、既にadminが作成済みで立ち上がらなかったりということがあるので、その際は `KEYCLOAK_USER` と `KEYCLOAK_PASSWORD` をコメントアウトして再起動してください。

```docker-compose.yml
version: '3.7'
services:
  keycloak:
    restart: always
    image: jboss/keycloak
    networks:
      - default
    environment:
      - KEYCLOAK_USER=admin
      - KEYCLOAK_PASSWORD=password
      - DB_VENDOR=postgres
      - DB_ADDR=keycloak-db
      - DB_DATABASE=keycloak
      - DB_USER=keycloak
      - DB_PASSWORD=password
    ports:
      - '8080:8080'
    tty: true
    stdin_open: true
    volumes:
      - ./keycloak_data:/opt/jboss/keycloak/standalone/data

  keycloak-db:
    image: postgres
    environment:
      - POSTGRES_DB=keycloak
      - POSTGRES_USER=keycloak
      - POSTGRES_PASSWORD=password
    volumes:
      - ./keycloak_db_data:/var/lib/postgresql/data

networks:
  default:

volumes:
  keycloak_db_data:
  keycloak_data:
```

立ち上がったら [http://localhost:8080/auth](http://localhost:8080/auth) をブラウザから開くとこんな感じになると思います。
![スクリーンショット 2023-03-26 12.50.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/3f18a597-8d0d-fbc8-703a-3ea96a154fb8.png)



左の Administration Console をクリックすると ID/Passの入力が求められるので、先程の dockerイメージに環境変数で設定した値 (admin / password ) を入力すると、このような画面に遷移します。
![スクリーンショット 2023-03-26 13.01.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/2812e241-9e6b-d9ec-dbcf-1256ff1f2357.png)


これがいわゆるIdPサーバーの管理画面ということになります。
試しに、このIdPの中に属するユーザーのアカウントを追加登録し、そのアカウントでログインできるか試してみましょう。

サイドバーのUsers → Add User から適当な情報を入れて作成します。
![スクリーンショット 2023-03-26 13.30.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/1c86d49a-ddcd-c90a-ea83-dd22d2e02a39.png)



作成後に Credentials タブに移って、Passwordの設定をします。確認なので適当でOK。
![スクリーンショット 2023-03-26 13.32.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/2b78e3e8-54ac-3bff-c094-66de58146c5f.png)


サイドバーのClientsに行くとパスの一覧があります。一番上の account に対応するURLをクリック。
![スクリーンショット 2023-03-26 13.31.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/8c4547da-6120-923c-a0b8-8644850d3bc5.png)


別タブでこんな画面に遷移しますが、まだadminでログインしている状態なので、一旦Sign Outします。
![スクリーンショット 2023-03-26 13.41.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/6b05877d-5e9b-9ed2-02f7-8499e5a112df.png)


同じ画面にリダイレクトされるので、左の Personal Infoをクリックするとログイン画面が現れます。ここで、先ほど作った適当なアカウントのID/Passでログインできれば確認完了です。

# 3. SPの情報を設定する

ここまでで無事IdPとして振る舞うことができるようになったので、次はServiceProviderの情報をIdPに登録する手順をやっていきます。再びadminのアカウントでログインし、管理画面を開きます。( [http://localhost:8080/auth/admin/master/console](http://localhost:8080/auth/admin/master/console) )

サイドバーの一番上のメニューから `Add Realm` ボタンを押して、新しいrealmを追加します。

おっと。唐突に realm という単語が出ましたね。
せっかくなので最近流行りのGPT4に説明してもらいましょう。

`Q: IdP内で設定する realm とは何かを簡潔に説明してください`

```
IdP (Identity Provider) 内で設定する "realm" とは、アプリケーションやサービスに対する認証および
認可を管理する独立したドメインです。簡潔に言えば、realmはユーザー、クライアント、ロール、および許可を定義し、
それらの間の関係を管理する単位です。KeycloakなどのIdPでは、realmは以下の目的で使用されます。

ユーザーの管理: ユーザーの認証情報、属性、グループメンバーシップを保持します。
クライアントの管理: アプリケーションやサービスがIdPに登録され、設定されます。
ロールと許可の管理: ユーザーやグループに割り当てられるロールや許可を定義し、アクセス制御を実現します。
認証および認可ポリシーの管理: ユーザーがどのように認証され、どのリソースにアクセスできるかを定義します。

realmを使用することで、異なる組織やプロジェクトがそれぞれのユーザーとアプリケーションの管理を独立して
行うことができます。また、共通のIdPを使用しながら、異なるアプリケーションやサービスで異なる認証および
認可ポリシーを適用できます。
```

とのことです。IdPの中では、このサービスにはこのSPとこのユーザーがいて…みたいな管理をしていくわけですが、それを束ねる一番上位の概念という感じでしょうか。もちろんこれはmasterを使っても良いのですが、rootっぽい概念なので基本masterには触らず、他のものを作っていくというのがお作法のようです。

ということで早速作っていきましょう。例によって名前は適当で構いません。
![スクリーンショット 2023-03-26 13.25.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/b89c7bd9-dc61-f4a5-ff7a-18bcc3383b38.png)


Createすると、Masterではなく新たに作ったrealmの画面になっています。
![スクリーンショット 2023-03-26 14.01.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/d3374ed0-6b52-a460-8fdf-9682b352a648.png)


次に、このrealmの中にSPの情報を追加しています。SPは、realmの中では `client` という概念として扱われます。サイドバーの clients から追加していきましょう。

**Client Protocol で `saml` を選択して** saveします。
![スクリーンショット 2023-03-26 22.53.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/05301d48-8b03-a272-d04c-d8631e0a7f96.png)


こちらの画面に遷移します。スクショした範囲には変更点はありません (デフォルト値のまま)。
![スクリーンショット 2023-03-26 22.53.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/a497150b-3844-9171-725b-2b81c8c57e77.png)


次のスクショ内には変更があります。
- Name ID Format で `email`  を選択し直しています。
- Base URL、 Valid Redirect URIs はそれぞれ適切な値を設定してください。

また、後述しますがIDP Initiated SSO URL Nameは最終的にSPへ渡すURLの一部になります。
![スクリーンショット 2023-03-26 23.02.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/f119b998-819c-fc73-fb21-025e1e9f0f2d.png)


ペインを開いて、POST Binding関連の項目にも同様のURLを入れておきます。
![スクリーンショット 2023-03-26 22.56.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/57eb0990-3cb7-8154-c24b-28d3bc1d262e.png)


ここまででclientの設定は完了です。(最後、下の方にある **save** を押し忘れがちなので気をつけてください!)

# 4. IdPからSPに渡す情報

連携を行う際にIdPで作成し、SPのサーバーで設定してもらう情報が二つあります。ログインURLと署名証明書 (Signing Certificate) です。

### ログインURL

先ほど設定した IDP Initiated SSO URL Name の下部に出ていたパスになります。
![スクリーンショット 2023-03-26 23.02.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254731/95320e0a-38ea-c6c9-1b17-a9fddd6b1452.png)



### 署名証明書

サーバー同士の認証を行うための証明書を発行します。これはKeyCloakの場合realmが設定するものになるため、Realm Settings → keys → active → certificate から取得することが可能です。一般的にはX.509形式を使うことになると思いますが、SP側で何を使うべきか適宜確認してください。

- サンプル
    
    ```
    -----BEGIN CERTIFICATE-----
    MIICqzCCAZaaa..(中略)
    -----END CERTIFICATE-----
    ```
    

# トラブルシュート

ここまでの情報をIdP、SPの双方で正しく反映できていればSAML連携での認証が通るはずです。ただ、最初はうまくいかないな〜とハマった部分多々あり、おおよそ以下のようなことが原因でした。

- 反映すべき項目が設定されていない
- 単純なtypo
- IdP/SPに関係ない部分での挙動 (デバッグのため通常やらないような更新作業をした結果、アプリケーションが意図通りに動かなくなる様なアカウント設定をしていた等々)

よく見直してもうまくいかない場合は、案外最初から全てをやり直した方がうまくいくことが多かったかなと思います。(設定項目が多いし、SPとIdPを行ったり来たりするので抜け漏れ間違いが発生しやすいかなと)

無理に現状キープを目指さず、やり直しながら理解を深めていくのがおすすめかなと思います。

### 参考情報
- Auth0が提供しているSAMLアサーションをデコードするツール
    - https://samltool.io
- ローカルのポートをインターネットに開放するツール
    - https://ngrok.com
    - https://qiita.com/mininobu/items/b45dbc70faedf30f484e
- ローカルのパケットをキャプチャーして中身をみるツール
    - https://www.wireshark.org/download.html
    - https://qiita.com/y-vectorfield/items/3aaaa98eb2aeca223ecf

これらを組み合わせると、 `ローカルに立てたIdPサーバーがローカルの外にあるSPとどのような通信を行なっているか` を簡単に監視することができます。


# おわりに
冒頭にも少し書きましたが、この辺りの　環境構築に関わる情報を検索すると、ヒットしてもちょっと古かったり、我々のユースケースにマッチしなかったり...で、ちゃんと動かすまでに意外と手間がかかったため、今回記事にまとめさせていただきました。
こういう手順書っぽいものってあまり表には出てこないかなとは思うのですがこれからSSOの実装・検証をされる方の一助になれば幸いです。

最後になりますが、グロービスではBE/FE問わず広く採用を行なっています。本記事で取り上げたような大きな企業との認証連携や、社会人教育というドメインを通じたエンドユーザーへの価値提供など、さまざまなissueと日々向き合っていますので、ご興味のある方はぜひ下記のリンクからお問い合わせください。
https://recruiting-tech-globis.wraptas.site

カジュアルな面談も行なっていますのでぜひお気軽に！