Keycloakを使ったOAuth2.0でAPIを保護をする

# はじめに

最近のweb開発ではマイクロサービスなどが採用される場面が増え、APIのサービスや機能を他システム・アプリと連携したりなどの機会が増えてきているように感じます。

そんな中、APIを保護するためにOAuth2.0などがよく選択肢に挙げられますが認可の仕組みを1から自前で構築するにはそれなりの実装コストが発生します。

そこで、今回は認可・認証機能を提供するOSS `Keycloak` を紹介しつつサービス全体の構成について触れていきたいと思います。

# Keycloakとは
簡単に言ってしまえば認証トークンの発行・管理ができる認可サーバーですが、様々な機能を提供しています。

- 主要な認可、認証などのプロトコルをサポート(OAuth2.0、OpenID Connect、SAML...)
- GitHub、Twitterなどのソーシャルログインに対応
- 既存のLDAPと連携可能
- 認証情報も保持できKeycloak単体で認証まで可能
- トークン、ユーザー、クライアントなどを操作可能なRest APIの提供
- ログイン画面の提供(カスタマイズ)
- どのユーザーがセッションにいるか確認でき、ログアウトなどの操作も可能
- Keycloakに設定された様々な情報のエクスポート・インポート
- etc....(さらに詳しい情報は[公式ドキュメント](https://www.keycloak.org/documentation)をご覧ください。)


さらに、これらの機能をKeycloakの管理画面から詳細に設定が可能になっています。(以下、例)

・クライアント[^1]管理
クライアントの認証フロー、トークンの発行権限、リダイレクトURL、ロール、ログインセッション管理など設定や、トークン要求をする側に制限を設けたりなどが可能です。
![client設定画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295284/b4e0f614-9cc7-3ff3-6319-da02fe8651af.png)

・セッション管理
セッション管理はユーザー単位でもどのクライアントでいつセッションが開始されたのか、その時点でのIPアドレスはなにかなどを確認でき、ログアウト操作も可能です。
![セッション管理画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295284/4363e256-121f-db6a-6416-cbf48a392066.png)


# 構成例
Keycloakを認可サーバーとしてOAuth2.0のAPI保護を実現する構成イメージ
![構成例](https://github.com/s-moteki/oauth2-with-keycloak/raw/main/docs/flow.jpg?raw=true)

今回は上記のような認可コードフローで取得したアクセストークンを用いてAPI Gateway経由でAPIへアクセスする流れを想定しています。
(※焦点を絞りたいためstateやPKCEなどは省略しています)

* **認可サーバー**
今回Keycloakが担当する部分でログイン画面の提供、認可コード、アクセストークンの発行・管理などを行います。

* **Client(アプリケーション)**
ログイン成功後、認可サーバーから認可コードを付与してリダイレクトされるので、Client [^1] は受け取った認可コードを使用して認可サーバーにアクセストークン要求のリクエストを行いアクセストークンを取得します。
[^1]: Keycloakではトークン・認証要求できるアプリケーションをClientと表現し、webアプリケーションなどが該当します。

* **API Gateway**
APIに対してリクエスト時に付与されたアクセストークンを認可サーバーに対して検証リクエストを行い、有効なアクセストークンだった場合のみAPIへリクエストを送るなどの制御を行います。

* **API**
今回のアクセストークンによる保護対象。
ClientからAPIにリクエストする際は必ずAPI Gatewayを経由させます。

## 実装・構築
先ほどの構成イメージをより具体的にするため、下記図の青線枠に絞って実装してみました。

![実装例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295284/305b6b80-b60d-4214-7e26-7087a4cb69d2.jpeg)

* **Keycloak**(認可サーバー)
  - [Docker Hubのイメージ](https://hub.docker.com/r/jboss/keycloak/)を使用
  - デフォルトはインメモリを使用するので、外部ストレージ(MySQL)に変更
  - 提供するログイン画面のデフォルトからデザインをカスタマイズ
  - Client、API Gatewayからトークンリクエストができるように設定しjsonファイル形式で設定値をエクスポート、コンテナ起動時にエクスポートされたjson形式の設定ファイルを読み込ませることで初回から設定済みの状態で起動

* **node.js**(API Gateway)
  - node.js、TypeScript、http-proxyを使用したリバースプロキシで経由させる
  - 保護するAPIへのアクセスはAuthorizationヘッダーからアクセストークンを取得し、Keycloakへのトークン検証リクエストの結果によりアクセスを制御

> ※当初Nginxで実装しようと思い、結果的には実現できたのですがモジュールの使い勝手や自由度が低い点から最終的にnode.jsで実装し直しました。

* **JSON Server**(API)
  - 今回の保護対象となるモック用API
  - 公開用とアクセス制限用の2つのエンドポイントを用意(/public , /private)

実装の詳細はこちらのリポジトリをご覧ください。

https://github.com/s-moteki/oauth2-with-keycloak


### ローカル環境での構築、アクセス制御の確認
環境構築後に先ほどの[構成例](#構成例)のフローに従って、保護されたAPIにアクセストークンを使用してリクエストを行います。

##### 1. 環境構築 
先ほどのリポジトリをクローンし、docker-composeで起動します。

```bash
# リポジトリのクローン
git clone git@github.com:s-moteki/oauth2-with-keycloak.git

# リポジトリに移動
cd oauth2-with-keycloak

# コンテナの起動
docker-compose up node-gateway
```

##### 2. アクセス制御

ます、アクセストークンがない状態で公開用、アクセス制限用の2つのエンドポイントを確認してみます。

```bash:公開APIリクエスト
curl http://localhost:8080/public
```

```json:公開APIレスポンス
{
  "message" : "ok",
  "access_type":"public"
}
```

保護対象のエンドポイントはまだアクセストークンないので正常レスポンスを取得できません。([フロー図.4~5](#構成例))
```bash:アクセス制限APIリクエスト
curl http://localhost:8080/private
```

```json:アクセス制限APIレスポンス
{
  "message": "unauthorized"
}
```

##### 3. アクセストークンの取得
では、本題のアクセストークンを取得してみたいと思います。
今回はClientの実装はないので認可コードの取得〜アクセストークンの取得までが手作業ですが、本来はアプリケーションの実装でパラメータから認可コードを取得、認可サーバーへアクセストークン要求リクエストの流れで処理をします。

**3-1** ログイン([フロー図.1](#構成例))
下記URLをブラウザで開き、初回自動生成されたユーザーの情報でログインします。
・ユーザー情報
username : test-user
password : password

> http://localhost:18080/auth/realms/test_service/protocol/openid-connect/auth?client_id=test_client&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2F&response_mode=query&response_type=code

**3-2** 認可コードの取得([フロー図.2](#構成例))

ログインに成功すると、画面遷移してURLに以下のような形式で認証コードがパラメータに付与されているので、そこから認証コードを取得する。
※実際にはアプリケーションにリダイレクトさせて認証コードを取り出す
> http://localhost:8080/?session_state=XXXX&code=認証コード

**3-3** アクセストークンの取得([フロー図.3](#構成例))

先ほど取得した認可コードでcurlコマンドの認可コード部分を置き換えてアクセストークン要求のリクエストをします。
```bash
curl http://localhost:18080/auth/realms/test_service/protocol/openid-connect/token \
-d 'grant_type=authorization_code&username=test-user&client_id=test_client&client_secret=wgefmNBGop63ctr564st1mDtWuNfP1Uw&code=認可コード&redirect_uri=http://localhost:8080/'
```
※認可コードは短命で認可コードが発行されてからしばらく経過するとリクエストに失敗するので、再度認可コードの取得を行ってください。
```json:レスポンス例
{
  "access_token":"アクセストークン",
  "expires_in":300,
  "refresh_expires_in":1800,
  "refresh_token":"リフレッシュトークン",
  "token_type":"Bearer",
  "not-before-policy":1643635658,
  "session_state":"XXXXXXXXXXXXXXX",
  "scope":"email profile"
}
```
##### 4. 保護されたエンドポイントへリクエスト([フロー図.4~6](#構成例))

先ほど取得したアクセストークンをヘッダーに指定してリクエストすることで保護したいAPIから正常レスポンスが取得できるようになります。
```bash:アクセス制限APIリクエスト
curl 'http://localhost:8080/private' \
--header 'Authorization: Bearer アクセストークン'
```
※当然ですがアクセストークンの有効期限が切れると正常レスポンスは取得できなくなります。
```json:アクセス制限APIレスポンス
{
  "message" : "ok",
  "access_type":"private"
}
```

これで[構成例](#構成例)のフローで保護されたAPIへのアクセスができるようになり、Keycloakを使用したAPI保護をひとまず実現できました。

## 最後に
Keycloak以前にOAuth自体(その他認証もですが)かなり奥が深く、全体的な構成・フローも様々なものが存在する中でこの記事だけでより詳しく触れるのは難しそうだったので、今回はなるべく簡潔に全体的な構成をメインに触れていきました。
しかし、今回触れていない部分(認可・認証関連、Keycloakコンテナのクラスタ環境、etc...)も本来だと意識しないといけないので、機会があったら触れていない部分を1つずつ記事にしていけたらなと思います。

当初ClientはパブリックなSPAなどのパターンやフローごとに実装内容が変わってくるので実装の範囲外としていたのですが、記事を書いている際に簡単なClientの実装例があっても良さそうに思えてきたので時間があればClientも例として実装していきます。

## 参考
[Keycloak公式ドキュメント](https://www.keycloak.org/documentation)
[Keycloak API ドキュメント](https://www.keycloak.org/docs-api/17.0/rest-api/index.html)
[Keycloak イメージ](https://hub.docker.com/r/jboss/keycloak/)