Auth0 と auth0-spa-js を使った独自Backend APIのアクセス制御方法の整理
Leaner Technologies という会社でB向けSaaSの開発をしている @corocnです。この記事は Digital Identity技術勉強会 #iddanceのカレンダー 13日目の記事です。
今年は会社単位のアドベントカレンダーはなくて、好きなカレンダーに参加しようぜってことになりました。私は懲りずに Auth0 の話を書こうと思います。
やりたいこと
SPA + 独自Backend + Auth0 な構成で、セッショントークンとして IDToken ではなく AccessToken を使用するための設定。
Auth0 は Web SPA 向けにSDK(auth0/auth0-spa-js)を提供しています。ドキュメントだけだと分かりそうで分からないと思うので、裏側の設定も含めて紹介します。ちなみに、auth0/auth0-react も auth0-spa-js を利用しています。
本記事に関連する設定項目
- Auth0
- Applications
- Applications
- APIs
- Permisssion
- User Management
- Users
- Roles
- Applications
Application設定のみの場合
まず Auth0のダッシュボード で SPA向けのApplication(いわゆるクライアント)を作成します。
callback URLなどを設定しておきます。今回は雑に Next.js で試したので、ローカルで立ち上がるデフォルトの localhost:3000 を設定しています。
ログインボタンをクリックすると Auth0 側へリダイレクトされ、戻ってきてユーザー情報や AccessToken を確認するようなコードを用意しました。
import type { NextPage } from 'next'
import { Auth0Client } from '@auth0/auth0-spa-js';
const auth0 = new Auth0Client({
domain: 'corocn-demo-2021-12.jp.auth0.com',
client_id: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx'
});
const Home: NextPage = () => {
const onClickLogin = async () => {
await auth0.loginWithRedirect({
redirect_uri: 'http://localhost:3000',
});
}
const onClickLogout = async () => {
auth0.logout({
returnTo: 'http://localhost:3000'
});
}
const getUserInfo = async () => {
const accessToken = await auth0.getTokenSilently();
console.log(accessToken);
const user = await auth0.getUser();
console.log(user);
const claims = await auth0.getIdTokenClaims();
console.log(claims);
console.log(claims?.__raw);
}
return (
<div>
<div>
<button onClick={onClickLogin}>Login</button>
<button onClick={onClickLogout}>Logout</button>
</div>
<div>
<button onClick={getUserInfo}>getInfo</button>
</div>
</div>
)
}
export default Home
まず getTokenSilently()
で取得した AccessToken を覗いてみます。
Payload 部分が抜け落ちたものになっています。ヘッダー部分にencが入っているのでJWEっぽいけど不正な形式ですね。
次に getUser()
で取得できるオブジェクトの中身を見てみます。
メールアドレスや名前などのprofileの情報が入っていますね。前提を書き忘れましたが Google 連携でログインできるようにしておいたので、subもそのような表現になっています。
getIdTokenClaims()
で取得できるIDTokenも覗いてみましょう。
モザイクだらけですが・・・audience の値は Auth0 の ClientID になっていますね。また、よく見ると __raw
に生のIDトークンがセットされています。
これを引っ張り出して 独自Backend API に投げたくなる気持ちもわかりますが、勘がいい人ならダブルアンスコからの「これ使ったらアカンやつでは?」と気付くでしょう。(ホントに?)
APIの定義
じゃあどうすればええねんという話です。
独自Backend向けの AccessToken を取得する方法ですが、まず Applications の設定の隣にある、APIs から設定を作成します。
今回は https://api.hoge.corocn.dev/
という API を叩くための AccessToken という想定でいきます。
getTokenSilently()
に audience をオプションとして与えてあげます。ここで指定する値は API の Identifier です。
const getUserInfo = async () => {
const differentAudienceOptions = {
audience: 'https://api.hoge.corocn.dev/',
};
const accessToken = await auth0.getTokenSilently(differentAudienceOptions);
console.log(accessToken);
}
取得できる AccessToken はJWT形式で、中を覗いてみると次のような情報が格納されています。
aud(audience) 部分が配列になっていますね。この AccessToken は次の目的で使用できそうです。
- 独自Backendへのアクセス
- Auth0 が所持しているユーザー情報へのアクセス
ここまで独自Backend向けの AccessToken を発行することができました。ついでに権限の制御まで踏み込んでみます。
scopeと権限の制御
Auth0 の RBAC(Role Based Access Control)の機能を利用して、発行する AccessToken を管理者用とメンバー用に分けてみます。
まずは API の設定です。RBAC を有効にします。
次に scope を追加します。
管理者はリードライト、メンバーはリードのみの想定です。
User Management の Roles からロールを追加します。admin と member にしてみます。
ロールに対してAPIの scope が紐付けられるので、それぞれ紐付けておきます。
管理者ロールの場合
ユーザーに管理者ロールを紐付けて生成される AccessToken を確認してみます。
確認用のコードです。getTokenSilently
のオプションに scope を追加しています
const getUserInfo = async () => {
const differentAudienceOptions = {
audience: 'https://api.hoge.corocn.dev/',
scope: 'read:all write:all' // ココ
};
const accessToken = await auth0.getTokenSilently(differentAudienceOptions);
console.log(accessToken);
}
リクエストの結果、read と write の scope が追加されました。
メンバーロールの場合
Auth0 のダッシュボードから、さきほどのユーザーをメンバーロールに変えてみて試してみます。scope には write:all を入れたままリクエストしてみます。
readだけになりました。ロールの紐付けがちゃんと効いてそうですね。
あとは、Backend側でスコープ等をちゃんと見て検証すればよさそうです。
おわりに
ritou先生のブログ そのIDTokenの正体はセッショントークン?それともアサーション? - r-weblife でも触れられていますが、混乱しやすい内容だなと思っています。Firebase 方面から来た人は特に。
私も昔はOpenID Connect由来の IDToken をセッショントークンとして使っていいと思っていた人間なので偉そうなことは言えませんが、参考になれば幸いです。
会社で Auth0 は利用してないけど採用情報も置いときます。ではでは。
Discussion