Auth0のRBAC(Role-Based Access Control)を使ってAPIのアクセス制御をやってみた
RBACとは
RBAC(Role-Based Access Control)とは、アクセス制御の方法の1つで、アクセスする主体(ユーザやサービス)に直接権限を設定するのではなく、ロールに権限を設定してアクセスする主体にはロールを設定する権限管理方法のことです。こうすることでアクセス主体ごとに権限を管理するよりも、ミスが起きにくく効率的に権限を管理できるなどのメリットがあります。代表的な例として、AWS IAMのロールを想像していただけるとわかりやすいかと思います。
Auth0でできること
注意: 現在、Auth0でRBACを実現する方法として Authorization Core
と Authorization Extension
の2つの方法がありますが、公式のアナウンスにて Authorization Core
に統合されることが予告されていますので、本記事でも Authorization Core
の内容だけを扱います。
Auth0のリソースとして、UsersとRoles、APIsの3つを利用します。
まずAPIsにて、CUSTOM APIを作成し、アクセス権限(Permission)の定義を行います。例えばCUSTOM APIには、タスクを参照するAPIと登録するAPIがあると仮定し、 read:task
, write:task
のようにそれぞれのAPIの実行を許可する権限(Permission)を定義します。これはアクセス制御を管理するための定義なので、どのような粒度でも構いません。
API | 実行を許可するPermission |
---|---|
タスク参照API | read:task |
タスク登録API | write:task |
次にRoleを作成し、権限(Permission)を割り当てます。例えば、 Reader
, Writer
というRoleを作成し、それぞれに上記の read:task
, write:task
の権限(Permission)を割り当ててみます。
Role | Permission |
---|---|
Reader |
read:task |
Writer |
write:task |
最後に、Userを作成し、上記のRoleを割り当てます。例えば、AというUserには Reader
ロールを、BというUserには Writer
ロールをという具合に割り当てます。
User | Role |
---|---|
A | Reader |
B | Writer |
こうすることで、AというUserが上記のCUSTOM APIに対するAccessToken(jwt)を要求したとき、そのclaimに permission
という属性が追加されます。AというUserは Reader
Roleで、 read:task
の権限が割り当てられているので、 permission
は [read:task]
となります。
{ "permissions": [ "read:task" ] }
API側では、リクエストヘッダーなどに設定されたAccessToken(jwt)から permission
を読み取り、実行を許可するかどうかを判定することができます。
[備考] claimにrolesを付与するのとどう違うのか?
Auth0ではRulesという機能を使うことで、Userが要求したAccessToken(jwt)のclaimに、Userが属するRolesの情報を追加することもできます。例えば、上記の例でいうと、claimに roles
という属性が追加され、値は [Reader]
となると仮定しましょう。
{ "roles": [ "Reader" ] }
API側ではこの roles
の値を元に実行を許可するかどうかを判定することもできます。一見するとこれでも良さそうに見えますし、実際にRoleが2つしかない状況などではこれでも十分だと思います。
API | 実行を許可するRole |
---|---|
タスク参照API | Reader |
タスク登録API | Writer |
しかし、もっとRoleの数が多く複雑な権限管理が必要になったときにこの方法では厳しくなってきます。例えば、新しい組織が増えRoleを追加しようとしたときに、この方法ではAPI(アプリケーション)側の修正が必要になりますが、Permissionで管理している場合はAuth0側に新しいRoleを追加して適切なPermissionを設定し、そのRoleを新しい組織のUserに割り当てるだけで良いのです。
やってみる
Auth0の設定
Tenantを登録
Auth0にSign Upし、新しいテナントを作成します。
APIsを登録
管理画面の APIs から、新しいAPIを登録します。
PermissionsタブでPermissionを設定します。
SettingsタブでRBACとAccessTokenへのPermissionの追加を有効化します。
Rolesを登録
管理画面の Users & Roles > Roles から、新しいRoleを登録します。
Usersを登録
管理画面の Users & Roles > Users から、新しいUserを登録します。
Applicationを登録
管理画面の Applications から、新しいApplicationを登録します。
種類は SINGLE PAGE APPLICATION
とします。
今回はサンプルアプリケーションを使用しますので、Settingsタブの Allowed Callback URLs
, Allowed Web Origins
, Allowed Logout URLs
に、それぞれ http://localhost:3000
を設定します。
ApplicationでAPIに対するAccessTokenを発行する
Application(SPA)にログインした後、APIを利用するためのAccessTokenを発行します。
ここが今回のはまりポイントでした。Application(SPA)にログインしたときに得られるAccessTokenには permission
属性は設定されないのです。なぜなら Permission は API によって定義されたもので、Application(SPA)の定義ではないからです。 permission
属性を含むAccessTokenを得るには、audienceをAPIとしてAccessTokenを要求する必要があります。(考えてみれば、ApplicationとAPIは異なるaudienceなのでApplicationで得たAccessTokenを使ってAPIにリクエストするのはおかしいですよね。)
サンプルアプリケーションを使ってやってみます。
ApplicationのQuick Startタブから、JavaScriptのサンプルアプリケーションをダウンロードします。
public/js/app.js の120行目辺りに以下のコードを追加します。scopeには openid
を指定します。これを指定しないとAccessTokenがjwtになりません。
const options = { audience: "http://rbac-example.com/", scope: "openid" }; const accessToken = await auth0.getTokenSilently(options); console.log("accessToken", accessToken);
アプリケーションを起動し、ブラウザでアクセスしてログインします。
ログイン後、コンソールにaccessToken(jwt)が出力されていれば成功です。
jwt.io で jwt のペイロードを確認して permissions
claim があれば成功です。
注意: "Consent required" のエラーになる場合
ブラウザに localhost ドメインでアクセスしてAPIのAccessTokenを取得しようとすると、"Consent required" というエラーになる場合があります。ならないときもあったのですが詳細な条件は不明です。このエラーは 公式のドキュメントでも説明されている のですが、APIのSettingsで"Allow Skipping User Consent"がEnabledになっているときでも、ローカルで起動しているアプリケーションは信頼できないからダメだよっていうことみたいです。回避方法として /etc/hosts
を書き換えて適当なドメインを設定する方法が紹介されているのですが、これをすると今度は auth0-spa-js の方が、いやいや 信頼できないドメインでは使えないよ とエラーになるんです。私は ngrok で localhost:3000 に適当な(一応信頼される)ドメインを割り当てて回避することができました。
APIの権限管理を行う
最後にAPIアプリケーションで permissions
claim による権限管理を行ってみます。今回はNode.jsで、適当にexpressのアプリケーションを作ります。雛形の作成に express-generator
を使うので、 npm install express-generator -g
しておきます。express
コマンドを実行して雛形が作成されたら、次に必要なコマンドが表示されるので、それも実行しておきます。アプリケーションが起動することを確認してください。
$ express --no-view --git auth0-rbac-example (中略) change directory: $ cd auth0-rbac-example install dependencies: $ npm install run the app: $ DEBUG=auth0-rbac-example:* npm start
次に、AccessToken(jwt)を検証できるようにミドルウェアを追加していきます。これはAuth0のAPIのQuick StartのNode.jsのサンプルがあるのでそれを、APIアプリケーションの app.js
に追加します。express-jwt
, jwks-rsa
の npm も追加しておきます。
@@ -2,11 +2,25 @@ var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); +var jwt = require('express-jwt'); +var jwks = require('jwks-rsa'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); +var jwtCheck = jwt({ + secret: jwks.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: '<CUSTOM APIのissuer>.auth0.com/.well-known/jwks.json' + }), + audience: '<CUSTOM APIのaudience>', + issuer: '<CUSTOM APIのissuer>', + algorithms: ['RS256'] +}); + var app = express(); app.use(logger('dev')); @@ -15,6 +29,8 @@ app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); +app.use(jwtCheck); + app.use('/', indexRouter);
次に同じく app.js
にタスク参照・登録APIのルーティングを設定します。
@@ -7,6 +7,7 @@ var jwks = require('jwks-rsa'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); +var tasksRouter = require('./routes/tasks'); var jwtCheck = jwt({ secret: jwks.expressJwtSecret({ @@ -32,5 +33,6 @@ app.use(jwtCheck); app.use('/', indexRouter); app.use('/users', usersRouter); +app.use('/tasks', tasksRouter); module.exports = app;
routes/users.js
をコピーして routes/tasks.js
を作ります。 GET /tasks
エンドポイントは read:task
permissionを必要とし、 POST /tasks
エンドポイントは write:task
permissionを必要とするようにします。express-jwt
ミドルウェアが、リクエストの Authorization: Bearer <AccessToken>
を検証し、 req.user
に jwt の claim を展開してくれているので、 req.user.permissions
でアクセスが可能です。
var express = require('express'); var router = express.Router(); router.get('/', function(req, res, next) { console.log(req.user); if (req.user.permissions.includes('read:task')) { res.send({ result: 'OK' }); } else { res.status(403); res.send({ result: 'Forbidden' }); } }); router.post('/', function(req, res, next) { console.log(req.user); if (req.user.permissions.includes('write:task')) { res.send({ result: 'OK' }); } else { res.status(403); res.send({ result: 'Forbidden' }); } }); module.exports = router;
APIアプリケーションを起動して、先ほど取得しておいたAccessToken(jwt)を使って、リクエストしてみます。
GET /tasks
エンドポイント
$ curl -X GET \ http://localhost:3000/tasks \ -H 'authorization: Bearer <AccessToken>' {"result":"OK"}
POST /tasks
エンドポイント
$ curl -X POST \ http://localhost:3000/tasks \ -H 'authorization: Bearer <AccessToken>' {"result":"Forbidden"}
permissionによってアクセス制御ができていることが確認できました。
所感
- API側は必要なpermissionだけ知っていれば良く、Auth0側でUser/Role/Permissionを管理できるので、うまく情報が分離されていて良いと思いました。過去に、User/Roleだけで複雑なアクセス制御を行って大変だった経験があるのでありがたみが分かります。
- Userに管理対象のリソースごとにRoleが設定されるようなアクセス制御が必要な(例えば、あるUserがリソースAに対しては管理者権限でリソースBに対しては一般権限のような)場合にはそのままは使えませんが、UserのmetadataとRulesを組み合わせれば、リソースごとにpermissionを返すことはできそうです。
- SPA側でもUserの権限に応じてアクセス制御(表示項目を変えたり)すると思いますが、SPA側のアクセス制御のベストプラクティスがあるのか気になりました。