HonoとCasbinで認可制御を実装する
こんにちは、sugar-catです。
先日、Honoの3rd PartyミドルウェアにCasbinを組み込みました。
この記事では、HonoとCasbinを組み合わせた認可制御のサンプルを紹介します。
HonoやCasbinについては、既に多くの解説記事がありますので、詳しくはそちらをご参照ください。
認可制御とは?
認可制御とは、システムやアプリケーションの特定のリソース
に対して、誰が
、何を
行えるかを制御する仕組みです。
たとえば、「自分が作成した記事は自分だけが編集できる」というようなルールを設定できます。
この仕組みは以下の3つの要素で構成されます。
- 誰が(Who): ユーザーやグループなど
- 何に対して(What): アクセス対象となるリソース
- どのような操作をするか(How): 読み込み、書き込み、削除などのアクション
これにより、特定のユーザーに特定の操作だけを許可する設定ができ、サービスの安全性を高めることができます。
認可制御やアクセス制御に関してはこちらの記事が詳しいので、ぜひご一読ください。
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
以下のすべての操作を行える一方で、ユーザーbob
はGET
メソッドのみ許可されていることを示しています。
Basic認証との連携
次に、HonoのBasic認証機能を利用して、認証後にCasbinを用いた認可制御を行う方法を紹介します。
クライアントは、Authorization: Basic {Base64Encoded(username:password)}
の形式で認証情報を送信します。
以下の例では、ユーザーalice
とbob
に異なるアクセス権限を設定しています。
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
に対してすべてのメソッドでアクセス可能ですが、bob
はGET
メソッドのみ許可されている状態になります。
ここでauthorizer: basicAuthorizer
のように、汎用的なhelperとしてbasicAuthorizer
が提供されています。
この処理では、Authorization Headerの内容をデコードし、CasbinのEnforcerに渡すためのユーザー名を取得しています。
内部では、Honoの組み込みBasic認証ミドルウェアのロジックを利用しています。
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.conf
のsub
に割り当てたい場合は、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を評価したい場合にも使用することが可能です。
カスタム認可ロジックの実装
もちろん、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