KeycloakとReBAC実装であるOpenFGAの連携を試してみよう

# はじめに

本記事では、Keycloakと近年注目を集めている新しいアクセス制御方式であるReBAC (Relationship-based Access Control) の実装の1つである、OpenFGAを連携させてアプリケーションのアクセス制御を行う例を紹介します。

:::note warn
本記事で紹介する方法は、まだPoCレベルのものです。
:::

# ReBAC (Relationship-Based Access Control) とは

ReBACとは、リソースへのアクセス権限がサブジェクトとリソースの間の関係性によって定義されるアクセス制御方式です。[Wikipediaの説明](https://en.wikipedia.org/wiki/Relationship-based_access_control)によると、ReBACという用語自体は2006年と随分前に登場しているようですが、ここ数年でたくさん聞くようになりました。そのきっかけといえば、2019年にGoogleより公開された論文「[Zanzibar: Google’s Consistent, Global Authorization System](https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/)」でしょう。Googleのサービス群を支える巨大な認可システムであるZanzibarでは、このReBACによるアクセス制御方式を実現したものでした。そしてこの論文からインスパイアを受けて、今回紹介するOpenFGAを含めたいくつかのOSSやプロダクト、サービスが誕生しています。

# OpenFGAとは

OSSのReBAC実装のひとつで、Auth0が提供するAuthorization as a Serviceである[Auth0 Fine Grained Authorization (FGA)](https://docs.fga.dev/intro/authorization-and-auth0-fga) のコア部分が[OpenFGAとして公開](https://auth0.com/blog/auth0s-openfga-open-source-fine-grained-authorization-system/)されて誕生しました。その後、CNCFのSandboxプロジェクトとして承認され、クラウドネイティブなReBAC実装として開発が進められています。

* GitHub: https://github.com/openfga/openfga
* ライセンス: Apache-2.0 license

# OIDC / OAuth 2との関係

OpenFGAなどのReBAC実装が提供する範囲は、OIDCやOAuth 2のスコープ外の領域です。OIDCはOPからRPに認証連携するところまでが担当範囲であり、アプリケーション側でのアクセス制御をどのように実現するかはノータッチです。一方、OAuth 2によるAPI認可では、アクセストークンをOAuthクライアントに渡し、リソースサーバのAPI呼び出しでアクセス制御に利用するため、関連があるように思えるかもしれません。しかしながらOAuth 2では、あくまでOAuthクライアントに対してAPIアクセス権を委譲するためのものであり、最終的にエンドユーザがアプリケーションを利用する際のきめ細かいアクセス制御に利用するのは推奨されません。このあたりの議論は以下の記事によくまとまっていますので、参照されるとよいでしょう。

* [Using scopes vs. permissions for application authorization](https://www.aserto.com/blog/scopes-vs-permissions-authorization)
* [On The Nature of OAuth2’s Scopes](https://auth0.com/blog/on-the-nature-of-oauth2-scopes)

一方で、2023年10月にOpenID Foundationにて[AuthZEN](https://openid.net/wg/authzen/)というWorking Groupが設立されましたので、これについて少し触れておきます。昨今、ReBAC実装も含めてですが認可をアプリケーションから切り離して外部化するプロダクトやサービスが数多く登場しています。OpenFGAと同じくCNCFの[Open Policy Agent (OPA)](https://www.openpolicyagent.org/) も、ReBAC方式ではありませんがそのひとつです。この分野の標準化は、20年前以上にXMLベースのプロトコルとして[XACML](https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=xacml)が存在しましたが、SAMLやOIDC、OAuthと比べると普及は限定的でした。このWorking Groupでは、ユースケースや認可方式の文書化、PEP-PDP間プロトコルの標準化に取り組んでいくとのことです。将来、OpenFGAを含めた様々なプロダクト間で相互接続性が高まるかもしれません。今後の動向に注目しています。


# KeycloakとOpenFGAの連携

KeycloakとOpenFGAを連携させるPoCプロジェクトとして、[Martin Besozzi](https://twitter.com/embesozzi)氏による[Keycloak integration with OpenFGA (based on Zanzibar) for Fine-Grained Authorization at Scale (ReBAC)](https://github.com/embesozzi/keycloak-openfga-workshop) が公開されていますので、今回はこれを試してみます。下図は、このサンプルのアーキテクチャ図です (出所：https://github.com/embesozzi/keycloak-openfga-workshop )。KeycloakとOpenFGA、そしてサンプルアプリケーションが含まれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/7aecab98-8d6b-69d0-7e8d-2b4e93ab9e8b.png)

サンプルアプリケーションは、Eコマースサイトの商品を管理するアプリケーションを模したものです。アプリにログインすると商品一覧を参照できます。商品は公開中かどうかを示すステータスを持っており、未公開の商品については公開するための更新オペレーションを実行できます。以下、商品一覧を参照する際の処理シーケンスになります (図中の番号と対応しています)。

1. ユーザは「App」にアクセスし、OIDCでKeycloakでログインして認証連携します。
2. ユーザが「App」の画面で「View product」というアクションを行うと、「App」はそのバックエンドである「API Products」に対して、OIDCで取得したアクセストークンを付与してREST APIを呼び出します。
3. 「API Products」はアクセストークンの検証に加えて、アクセストークンに紐づいているユーザ (つまりログインしたユーザ) が「View product」の権限を持っているかどうかを、OpenFGAに問い合わせて確認します。


このサンプルではKeycloakでユーザとレルムロールを管理し、そしてユーザとレルムロール、レルムロールとレルムロールのアサイン関係をKeycloak上で結ぶタイミングで、その情報をOpenFGAに登録するアーキテクチャになっています。レルムロールでは商品の参照権限を表す`view-product`、更新権限を表す`edit-product`と、それらのレルムロールを含むビジネスロールとして`admin-catalog`、`analyst-catalog`の合計4つのレルムロールを定義しています。KeycloakではComposit Roleという、ロールの中にロールを含める機能がありますので、`admin-catalog`は`view-product`と`edit-product`の両方を含み、`analyst-catalog`は`view-product`のみを含むように設定されています。これにより、`analyst-catalog`をアサインしたユーザに関しては`view-product`権限しかなく更新処理は実施できない、ということを表現しようとしています。

KeycloakからOpenFGAへの上記関係データの登録は、「Event Publisher」という形で連携するアーキテクチャになっています。KeycloakのアドオンとしてEventListener SPIを実装した[Keycloak OpenFGA Event Publisher](https://github.com/embesozzi/keycloak-openfga-event-publisher) が組み込まれており、このアドオンはKeycloakのレルムロールのアサイン処理のイベントをフックして、その内容をOpenFGAにプッシュするようになっています。

:::note warn
現状このアドオンは、障害ケースにうまく対応できないのが気になるところです。KeycloakからOpenFGAへのプッシュが何らかの理由で失敗した場合に、それをリトライしたり、別途同期するような仕組みが現状はありません。
:::


## さっそく動かしてみる

GitとDocker (Docker Compose) が利用できる環境であれば、すぐに試すことができます。

```bash
# git clone
git clone https://github.com/embesozzi/keycloak-openfga-workshop
cd keycloak-openfga-workshop

# 起動
docker-compose -f docker-compose.yml -f docker-compose-apps.yml -f docker-compose-openfga.yml -f docker-compose-import.yml up
```

また、ブラウザでアクセスする端末の`hosts`ファイルに以下を追加しておきます。

```
127.0.0.1  keycloak openfga store store-api
```

## 動作テスト: 参照権限があるユーザでアクセス

1. 「App」のURL http://store:9090/ にアクセスします。右上の「LOG IN」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/3ee1425b-db74-a83e-7682-7b7379d7f2cc.png)

2. `paula`でログインします。パスワードは`demo1234!`です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/a61cccda-2809-217f-eb69-9aafacbd4bd2.png)

3. このユーザは商品を参照する権限を持っていますので、商品一覧を参照することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/ccfd5587-ee47-a993-fde8-df67f0b0f370.png)

4. 適当な商品の`PUBLISH`というリンクをクリックします。すると、以下のように権限のない旨のエラーが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/2263a86f-2ab3-6621-0b14-cc49ac7b9667.png)


## 動作テスト: 更新権限があるユーザでアクセス

一度ログアウトし、今度は`richard`でログインします (パスワードは同じく`demo1234!`)。先程と同じように`PUBLISH`リンクをクリックすると、今度は成功メッセージが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/b96b6d03-061e-1a1f-c809-5db376079d11.png)

※注意：サンプルアプリで実際に商品ステータスを更新する処理は実装されていないため、リンクをクリックしても`PUBLISHED`にはなりません。


## 動作テスト: 参照権限もないユーザでアクセス

同様に、今度は`peter`でログインします (パスワードは同じく`demo1234!`)。すると、このユーザは参照権限すら持たないため、商品を何も参照することができません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/06a740c9-9cdf-a583-589a-4a9d9e22ff5e.png)

## OpenFGAを覗いてみる

http://localhost:3000/playground をブラウザで開くと、OpenFGAが提供するプレイグラウンド機能が利用できます。ここでは、今回のサンプルアプリのReBACモデルの確認と評価を、画面から確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/ddddbda4-5a3b-1cfe-671f-9e91993a3e1e.png)

このサンプルでは、以下のReBACモデル定義がされています。ReBACではこのように、アプリケーションのモデル定義を行い、その定義したリレーションに基づいてアクセス制御の判断 (認可判断) を自動的に行います。なお、この定義は専用のDSLを利用することが多いですが、残念ながら標準的なものは存在しないため、現状ではReBACの実装ごとにそれぞれの文法を覚える必要があります。

```
model
  schema 1.1

type group
  relations
    define assignee: [user]

type role
  relations
    define assignee: [user] or assignee from parent or assignee from parent_group
    define parent: [role]
    define parent_group: [group]

type user
```

サンプルでは、`user`、`role`、`group`という3つのオブジェクトタイプを定義していますが、アプリケーション内で現状使われているのは`user`と`role`のみです。ポイントは、`role`のリレーション定義から抜粋した以下の部分です。

```
    define assignee: [user] or assignee from parent
    define parent: [role]
```

* 1行目：`assignee`というリレーションで、`user`タイプの要素と関係性を持っています。つまり、ロールはユーザに`assignee`というリレーションでアサインされることを示しています。また、OR条件で`assignee from parent`と書いてあります。OpenFGAでは、[間接的なリレーションを`X from Y`という表記で表すことができます](https://openfga.dev/docs/configuration-language#referencing-relations-on-related-objects)。これにより、`parent`リレーションで結ばれた親ロールから`assignee`リレーションで関係性のあるオブジェクトに対しても間接的にリレーションがあることを示しています。
* 2行目：`parent`というリレーションで、`role`タイプと関係性を持っています。つまり、ロールは親ロールを持てる構成になっていることを示しています。

モデルの定義に加えて、画面左下にはOpenFGAに登録されたタプル (定義したモデルに対する実際の関係データの組み合わせ) の一覧を参照することができます。これらのタプルは、前述で紹介した[Keycloak OpenFGA Event Publisher](https://github.com/embesozzi/keycloak-openfga-event-publisher)により、KeycloakからOpenFGAにプッシュされたデータ群です。サンプルでは最初から以下のように5つのタプルが登録されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/f1aade00-eaf8-8ca2-6b27-d965b3a024c2.png)

これらの関係性を図示すると、以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/37b983e1-94f1-65a7-62a4-29561e5d49b5.png)

:::note info
このサンプルのモデル定義は、OpenFGAや他のReBAC実装のサンプルモデルと比べると、違和感を感じるかもしれません。というのも、このサンプルプロジェクトはKeycloakのユーザとレルムロール (とグループ) の関係性をタブルとしてOpenFGAに登録する都合上、その関係性をそのままReBACのモデルとして実装しているためです。
よりReBACらしいサンプルモデルとしては、OpenFGAの[Modeling Guides](https://openfga.dev/docs/modeling)を一読するとよいでしょう。Google DriveやGitHub、Slackという馴染みのあるサービスを例にモデル定義例も紹介されています。
:::

このように、`view-product`と`edit-product`という権限を表す末端ロールはユーザに直接関連付けられてはいません。このような関係性であっても、**ユーザが間接的に`view-product`や`edit-product`ロールを持っているかどうかをリレーションのグラフを辿って自動的に評価してくれます**。この点がReBACのメリットのひとつです。

# アプリケーションのアクセス制御の実装

最後に、アプリケーション側で実施しているOpenFGAを利用したアクセス制御箇所についても見ておきましょう。バックエンドである「API Products」は、Node.jsでExpressを利用して実装されています。OpenFGAでは[Node.js用のSDK Client](https://github.com/openfga/js-sdk)を提供しているため、これを利用した[Middlewareを実装](https://github.com/embesozzi/keycloak-openfga-workshop/blob/ba93d124973ecfdc7f9cd068c05fc9d2246eef1c/store-openfga-api/src/middlewares/openfga.js)して組み込み、API呼び出し時にアクセス制御を実装しています。具体的には、[OpenFGAのRelationship Queries (Check API)](https://openfga.dev/api/service#/Relationship%20Queries) を利用して権限があるか (リレーションを持っているか) をOpenFGAに問い合わせ、結果として `true` or `false` を受け取っています。

```js
const checkTuple = async function (user, relation, object) {
    console.log(`[Store API] Check tuple (user: '${user}', rel: '${relation}', obj: '${object}')`);
    try {
        let client = await getClient();
        let { allowed } = await client.check({
            tuple_key: {
                user: user,
                relation: relation,
                object: object
            }
        });
        console.log(`[Store API] Check tuple for user: ${user} isAllowed: ${allowed}`); 
        return allowed;
    } catch ( e ) {
        console.log(e);
        return false;
    }
}
```

作成されたMiddlewareを[Router](https://github.com/embesozzi/keycloak-openfga-workshop/blob/ba93d124973ecfdc7f9cd068c05fc9d2246eef1c/store-openfga-api/src/routes/products.route.js#L26-L33)にて以下のように組み込み、使用しています。商品の公開操作は、ログインユーザに`edit-product`のロールがリレーションを辿って存在するかチェックしています。

```js
router.route('/:id/publish')
    .post(
        [ 
            jwt.validateToken,
            jwt.decodeToken,  
            fga.checkUserHasRole("edit-product")
        ],
        productsController.publish);
```


# まとめ

KeycloakとReBACのOSS実装であるOpenFGAとの連携について、PoCプロジェクトを一例に紹介しました。このサンプルではKeycloakのロールモデルをそのままReBACモデル化しているため、あまりReBACらしさを感じないサンプルにはなってしまっているのが少々残念なところです。ReBACに興味を持たれた方は、是非[公式ドキュメント](https://openfga.dev/docs/authorization-and-openfga)を参照してみてください。ReBACはアプリケーションによってははまると強力なツールになる可能性を秘めているのではないか、と期待していますので、引き続きウォッチしていきたいと思います。