❤️‍🔥

HonoとCasbinで認可制御を実装する

2024/10/01に公開

こんにちは、sugar-catです。

先日、Honoの3rd PartyミドルウェアにCasbinを組み込みました。
https://github.com/honojs/middleware/pull/676
https://github.com/honojs/middleware/tree/main/packages/casbin

この記事では、HonoとCasbinを組み合わせた認可制御のサンプルを紹介します。

HonoやCasbinについては、既に多くの解説記事がありますので、詳しくはそちらをご参照ください。
https://future-architect.github.io/articles/20221004a/
https://zenn.dev/aishift/articles/a3dc8dcaac6bfa

認可制御とは?

認可制御とは、システムやアプリケーションの特定のリソースに対して、誰が何を行えるかを制御する仕組みです。
たとえば、「自分が作成した記事は自分だけが編集できる」というようなルールを設定できます。

この仕組みは以下の3つの要素で構成されます。

  1. 誰が(Who): ユーザーやグループなど
  2. 何に対して(What): アクセス対象となるリソース
  3. どのような操作をするか(How): 読み込み、書き込み、削除などのアクション

これにより、特定のユーザーに特定の操作だけを許可する設定ができ、サービスの安全性を高めることができます。
認可制御やアクセス制御に関してはこちらの記事が詳しいので、ぜひご一読ください。
https://zenn.dev/she_techblog/articles/6eff1f28d107be
https://www.ipa.go.jp/security/vuln/websecurity/access-control.html

Casbin Middleware for Honoの使い方

それでは、HonoとCasbinを組み合わせて、実際に認可制御を実装する方法を見ていきましょう。
ミドルウェアにcasbinを組み込むことで、アプリケーションレイヤーのルーティング部分で認可制御が可能です。

インストール

まず、必要なパッケージをインストールします。Hono本体、Hono用のCasbinミドルウェア、そしてCasbin自体をインストールします。

npm i hono @hono/casbin casbin

Casbinモデルとポリシーの設定

Casbinを使用するには、まず「モデルファイル」と「ポリシーファイル」を設定します。モデルは認可制御のルールを定義し、ポリシーは誰がどのリソースに対してどのような操作ができるかを定義します。

model.conf(モデルファイル)

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")

このモデルファイルでは、リクエスト、ポリシー、ポリシーの効果、そしてマッチングルールを定義しています。具体的には、sub(誰が)、obj(何に対して)、act(どの操作を行うか)をもとにマッチングルールを設定し、ポリシーが許可されるかどうかを判断します。

policy.csv(ポリシーファイル)

p, alice, /dataset1/*, *
p, bob, /dataset1/*, GET

このポリシーファイルでは、ユーザーalice/dataset1以下のすべての操作を行える一方で、ユーザーbobGETメソッドのみ許可されていることを示しています。

Basic認証との連携

次に、HonoのBasic認証機能を利用して、認証後にCasbinを用いた認可制御を行う方法を紹介します。
クライアントは、Authorization: Basic {Base64Encoded(username:password)}の形式で認証情報を送信します。

以下の例では、ユーザーalicebobに異なるアクセス権限を設定しています。

import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'
import { newEnforcer } from 'casbin'
import { casbin } from '@hono/casbin'
import { basicAuthorizer } from '@hono/casbin/helper'

const app = new Hono()

app.use('*',
  basicAuth({
    username: 'alice', password: 'password',
  }, {
    username: 'bob', password: 'password',
  }),
  casbin({
    newEnforcer: newEnforcer('model.conf', 'policy.csv'),
    authorizer: basicAuthorizer
  })
)

app.get('/dataset1/test', (c) => c.text('dataset1 test')) // aliceとbobがアクセス可能
app.post('/dataset1/test', (c) => c.text('dataset1 test')) // aliceのみがアクセス可能

このコードでは、alice/dataset1/testに対してすべてのメソッドでアクセス可能ですが、bobGETメソッドのみ許可されている状態になります。

ここでauthorizer: basicAuthorizerのように、汎用的なhelperとしてbasicAuthorizerが提供されています。
この処理では、Authorization Headerの内容をデコードし、CasbinのEnforcerに渡すためのユーザー名を取得しています。
https://github.com/honojs/middleware/blob/main/packages/casbin/src/helper/basic-auth.ts

内部では、Honoの組み込みBasic認証ミドルウェアのロジックを利用しています。
https://github.com/honojs/hono/blob/c0d782cd649525ce40489906a24d83607deede29/src/utils/basic-auth.ts#L1-L26

JWT認証との連携

次に、JWTを利用した認可制御の例を紹介します。何らかのIdPで認証後に発行されたToken(JWT)を利用して認可制御を行うシナリオで使用できます。

以下の例では、JWTのペイロード内のsubをユーザー名として利用し、認可制御を行います。

import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
import { newEnforcer } from 'casbin'
import { casbin } from '@hono/casbin'
import { jwtAuthorizer } from '@hono/casbin/helper'

const app = new Hono()

app.use('*',
  jwt({ secret: 'it-is-very-secret' }),
  casbin({
    newEnforcer: newEnforcer('model.conf', 'policy.csv'),
    authorizer: jwtAuthorizer
  })
)

app.get('/dataset1/test', (c) => c.text('dataset1 test')) // aliceとbobがアクセス可能
app.post('/dataset1/test', (c) => c.text('dataset1 test')) // aliceのみがアクセス可能

jwtAuthorizerを使用することで、デフォルトではJWT内のsub Claimをもとに認可制御を行います。
また、特定のClaimをmodel.confsubに割り当てたい場合は、claimMappingを指定することができます。

const claimMapping = {
  ServiceRole: 'role', // keyは人間が理解しやすい名前、valueはJWTのClaim名
}
casbin({
  newEnforcer: newEnforcer('model.conf', 'policy.csv'),
  authorizer: (c, e) => jwtAuthorizer(c, e, claimMapping)
})

内部的にclaimMappingで指定した値を全てEnforcerに渡す実装となっているため、より複雑なモデルで複数のClaimを評価したい場合にも使用することが可能です。
https://github.com/honojs/middleware/blob/1ed5c7d7fad739be31d7a0b2280b8517b4637f3b/packages/casbin/src/helper/jwt.ts#L32-L35

カスタム認可ロジックの実装

もちろん、authorizerは自分で実装することも可能です。

import { Hono } from 'hono'
import { newEnforcer } from 'casbin'
import { casbin } from '@hono/casbin'

const app = new Hono()

app.use('*', casbin({
  newEnforcer: newEnforcer

('model.conf', 'policy.csv'),
  authorizer: async (c, enforcer) => {
    const { user, path, method } = c
    return await enforcer.enforce(user, path, method)
  }
}))

まとめ

HonoとCasbinを組み合わせることで、シンプルな認可制御を簡単に実装することができます。

Discussion