keyclaokの認可機能を利用する

keycloakと連携した認証認可制御を行う場合、Open ID Connectの認可フローを実装することが多い。
keycloak側で好きなclaimとscopeの設定ができるため、ほとんどの場合はアクセストークンの情報だけで認可制御ができる。
しかし、より細かな認可を行うために、keycloakではより細かな認可制御を行う機能が提供されている。
（例えば時間や条件に応じてアクセス可否を変更したい等）

:::note
keyclaokの公式ドキュメントにはjavaを利用したpolicy enforcerの実装が出てくるが、webを漁っても他の実装がなかなか出てこなかったので、メモがわりに。。
:::

作ったもの

https://github.com/m-masataka/keycloak-authorization-demo

# keycloakの認可
keycloakの認可機能ではAttribute-based access control (ABAC)やRule-based access controlに対応した様々な認可機能を備えている。

https://www.keycloak.org/docs/latest/authorization_services/index.html#_overview

下図はkeycloakにおける認可制御のアーキテクチャ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/bad653e9-4218-4bea-620a-595a2578e48d.png)
若干わかりにくいが、「PAP」「PDP」はkeyclaokが持っている機能で、PEPはkeycloakと連携するクライアント側（keyclaok-adopterと呼ばれている）で実装すべき機能となっている。

PEPとかPAPが何を示しているのかは以下の記事がとても参考になった。

https://kenfdev.hateblo.jp/entry/2020/01/13/115032

# OIDCの設定
細かな認可機能設定する前段として、ウェブアプリとの認証連携部分の設定を行う。
今回はOIDCで認証認可を実現する。

## keycloakの設定
keycloakでは管理するアプリケーションをclientという形で登録する。
まず最初にclientの登録から行う

### clientの登録
keycloakでclientを登録する。
keycloakの認可機能を利用する場合のポイントは１点で、
``Client authentication``と``Authorization``をOnにするだけ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/f6f7fa73-6299-c28b-9397-95693f09cf15.png)

他の設定は必要に応じて編集するが、今回はアプリ連携のために以下のようにリダイレクト/ログアウトurlのみ設定する。
|項目|url||
|-|-|-|
|Valid redirect URIs|http://localhost:3000/callback|リダイレクト用|
|Valid post logout redirect URIs|http://localhost:3000/|ログアウト処理後にリダイレクトされるURI|

## webアプリの作成
openid-clientを利用してkeycloakとoidc連携を行う。

https://www.npmjs.com/package/openid-client

ウェブアプリ側ではkeyclaokに接続するための設定、および、ログイン画面へのリダイレクトとcallbackを処理するためのエンドポイントを作成する。
はじめにkeycloakと通信するclientを作成する。
```javascript
import { Issuer, generators } from 'openid-client'
...

const keyclaokClientId = 'demo01'
const keyclaokClientSecret = '...'
const keycloakBaseUrl = 'http://localhost:18080'
const keyclaokRealmUrl = keycloakBaseUrl + '/realms/master'
const configUrl = keyclaokRealmUrl + '/.well-known/openid-configuration'
const appBaseUrl = 'http://localhost:3000'
const appCallbackPath = '/callback'
const appLogoutPath = '/logout'
const appCallbackUrl = appBaseUrl + appCallbackPath

// OIDCで、は当該 OpenID プロバイダーの情報を取得することが可能。
// keycloakでは http://..../realms/{realm name}/.well-known/openid-configuration
const keycloakIssuer = await Issuer.discover(configUrl);
console.log('Discovered issuer %s %O', keycloakIssuer.issuer, keycloakIssuer.metadata);

const client = new keycloakIssuer.Client({
  client_id: keyclaokClientId,
  client_secret: keyclaokClientSecret,
  redirect_uris: [appCallbackUrl], // webアプリ側でcallbackを受け取るURI
  response_types: ['code'],
});
```

keyclaokへのリダイレクトURLを設定する。
```javascript
// keycloakのログイン画面へリダイレクトURLを取得する
let authorizationUrl = client.authorizationUrl({
  redirect_uri: appCallbackUrl,
  // ↓はoidcで初期設定されるscope
  scope: 'openid email profile'
});

app.get('/login', function (req, res) {
  // keycloakへリダイレクト
  res.redirect(authorizationUrl);
})
```

最後にcallback処理のエンドポイントを作成する。
ここでトークンが取得できるため、保護対象のリソースへアクセスが可能になる。
```javascript
app.get("/callback", (req, res) => {
  const params = client.callbackParams(req)
  client.callback(appCallbackUrl, params, { code_verifier })
    .then(function(token){
      // アクセストークン・IDトークンが取得できるので、
      // 必要に応じてブラウザに返したり、セッションに保持したりを考える
      ...
      // トークンをセットして保護対象のリソースにアクセスする
      res.redirect(appHomeUrl)
    })
    .catch(function(error) {
      console.log(error)
      // エラーの場合はログイン画面に戻す
      res.redirect(appBaseUrl);
    })
})
```

ここまででOIDC連携の一連の流れが完了する。
![keyclaok-loing.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/526857a5-20d0-c2b8-feb7-cca9ad079b21.gif)

# 認可設定
ここからやっと認可設定に入っていく。

[Clients] -> [該当 Client] -> 「Authorization」タブへと進み、認可制御の設定を行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/0746b2b9-2f22-a979-8df0-7051704a32aa.png)

いくつか設定があるが、最低限の設定は以下の３つ
1. リソースの作成
1. ポリシーの作成
1. パーミッションの設定
    1. リソースとポリシーの紐付け

まずはじめにポリシーの登録を行う。
画面を見ると「Default Resource」がはじめに登録されている
> The default configuration consists of:
> - A default protected resource representing all resources in your application.
> - A policy that always grants access to the resources protected by this policy.
> - A permission that governs access to all resources based on the default policy.

全てのリソースに対する許可ポリシーとのことなので、とりあえずデフォルトポリシーは無効化しておくのが良さそう。

## リソース定義
保護するリソースの情報を定義する。
ここではリソース名（任意）とそれに紐づくURIを定義しておく。
- name
    - リソースを識別可能なユニークな名前を定義する
- uris
    - リソースのパスを設定（webアプリで利用）
- （Authorization scope）
    - リソースに紐づくscopeを設定する。今回は使わない
- （User-Managed access enabled）
    - ここをenableにすることで、リソースオーナからのリソース制御が可能になる。（UMAの認可フローを使う場合はここをenableにするが、今回はどちらでも）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/241ea9b1-f137-d8a4-53d3-0534f3a92386.png)

## ポリシーの定義
[Policies]タブで「Create Policy」をクリックするとポリシータイプの選択画面が出てくる。
個人的にここが充実してるのがとても良いところ。
（この辺りの細かいポリシー制御機能を、自前で作るとかなりコストが高いので）
何を選んでも良いが今回は「Role」を選択する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/82e0153e-02d7-3360-ecbc-528756ad4a2a.png)

Role based policyでは「role hogeのユーザはアクセス可/不可」のようなポリシーを作成できる。
（以下の例では「role demo-role01のユーザはアクセス可能」というポリシーを設定している。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/161bd141-9730-2461-1a7c-1d97a2547f2b.png)


https://www.keycloak.org/docs/latest/authorization_services/index.html#_policy_overview

## パーミッションの定義
パーミッションを定義する。
パーミッション定義では、作成したリソースとポリシーを多対多で関連付けることができる。
ここでリソースと、リソースに適用するポリシーを登録することで、１つのリソースに対し複数のポリシーを組み合わせて定義することが可能。
この辺りはxacmlにおけるruleとconditionの関係に似ている。

https://atmarkit.itmedia.co.jp/ait/articles/0212/11/news002.html

（以下の例では「role demo-role01のユーザはresource01（/protected01）にアクセス可能」というルールを定義している）


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/070e36b4-df9d-eccb-3f32-dbbf0d45b29a.png)


## Webアプリ側での認可制御
ウェブアプリ側ではいわゆるPE（policy enforcer）を実装する。
keyclaokの公式ドキュメントにはjavaでのpolicy enforcerの実装が載っているが、わざわざこのためにjavaを使うのは面倒のなので``nodejs``からkeycloak APIを直に叩いて認可を実装する。
公式のやり方としては、UMA（User-Managed Access）のフローに従い``Permission Ticketの取得 -> RPTの取得``という流れで認可制御を行うが、ここでは簡易的にPermission Ticket取得の流れを省いて認可を行う。

:::note warn
UMAではリソースサーバがpermission ticketを発行し、client側は発行されたticketを元に認可リクエストを行う。
このフローにより、permission ticketとkeycloakに定義したリソースが紐づき、clientとしてはkeycloakの管理情報を知らなくても認可リクエストが可能。
今回は、keycloakに設定されている``resources``と``client_id``をウェブアプリ側（Client）が知っている前提で、なんちゃってUMAを実装する。
:::

### RPT取得
はじめに、アクセストークンを元にRPTを取得する。
keyclaokに以下のリクエストを送ると、``access_token``としてRPTが返却される。
```bash
curl -X POST \
  http://${host}:${port}/realms/${realm}/protocol/openid-connect/token \
  -H "Authorization: Bearer ${access_token}" \
  --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  --data "audience={resource_server_client_id}"
```

fetchを使って以下のように書く。
accessTokenの部分はoidcのフローデー取得したアクセストークンを利用する。
```javascript
    const obj = {
      grant_type : "urn:ietf:params:oauth:grant-type:uma-ticket",
      audience: keyclaokClientId
    }
    const url = keyclaokRealmUrl + '/protocol/openid-connect/token'
    const rpt = await fetch(url,{
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${req.session.accessToken}`,
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: Object.keys(obj).map((key)=>key+"="+obj[key]).join("&")
      })
      .then(response => response.json())
      .then(result => {
        console.log(result)
        return result
      }).catch((error) => {
        console.log(error)
        throw error
      })
   // rpt.access_tokenとしてRPTが返却される
```

返却されたRPTの中身は以下のようになっており、`authorization`としてアクセス可能なリソース名が入ってくる。
これを参照することで自信がどのリソースにアクセス可能かを判断できる。
```json
{
  ...
  "authorization": {
    "permissions": [
      {
        "rsid": "f3aea62e-eb20-4694-9403-543ab6484140",
        "rsname": "protected01"
      }
    ]
  }
  ...
}
```

### 認可判定
webアプリ側では、このトークンを元にリソースへのアクセスを許可するか否かを判定できる。
また``permission=[対象のリソース名]``とし、response_modeを``decision``と指定することで、シンプルに入力可否だけを判定することもできる。
```javascript
    const obj = {
      grant_type : "urn:ietf:params:oauth:grant-type:uma-ticket",
      audience: keyclaokClientId,
      permission: "protected01",
      response_mode: "decision"
    }
```
レスポンスはtrue/falseで帰ってくる。
```javascript
{ result: true }
```

### scopeの利用
上記の例ではリソースに対してのアクセス可否のみ判定できる。
現実的にはアクセス可否だけでなく、read/write等のその他諸々のactionを判定する必要があったり、そんなに単純ではない。
scopeを利用することで上記RPTの認可情報にもう少し細かな権限情報を付与できる。

例えば、以下のように``read`` ``write``のscopeを作成し、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/589c83d8-8ad2-1fcc-4621-559c56278154.png)
リソースに対してスコープを紐付ける
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/3b6efa71-5586-4a36-e2f3-35a7bf28933f.png)

するとRPTには以下のようにscopeの情報も返却される。
これを使えばパス・スコープでそこそこ細かい粒度の認可も設定できそう。
```json
  "authorization": {
    "permissions": [
      {
        "scopes": [
          "read",
          "write"
        ],
        "rsid": "f3aea62e-eb20-4694-9403-543ab6484140",
        "rsname": "protected01"
      }
    ]
  },
```
scopeについても``response_mode=decision``での真偽値判定ができ、``permission= "{リソース名}#{スコープ}"``としてリクエストすることで認可判定が可能。
```javascript
    const obj = {
      grant_type : "urn:ietf:params:oauth:grant-type:uma-ticket",
      audience: keyclaokClientId,
      permission: "protected01#update",
      response_mode: "decision"
    }
```
```javascript
{ result: true }
```

ちなみに、エラーの場合は以下のように帰ってくる
```javascript
{
  error: 'invalid_scope',
  error_description: 'One of the given scopes [update] is invalid'
}
```

RPTにはリソースに設定したURLsの情報が入ってこない。
リソースオーナー = webアプリ開発者であり、webアプリ側でリソース名を知っている前提であれば上記で認可判定が可能。
そうでない場合はUMAの正式なフローに乗っかる必要がありそう。
UMAのフローに則った実装はまたの機会に。

# まとめ
keyclaokの認可機能を利用して簡単な認可機能を試してみた。
複数条件を指定したアクセス制御が簡単に実装できるので有用だと思う。
（保護リソースと認可情報の全てをkeycloakに登録しなければいけないのは手間かも）

次はUMAのフローに乗っかった実装をやってみます。