はじめに
Auth0のサンプルアプリを使って以下のことをやってみました。
- Single Page Applicationに認証機能を組み込み
- 認証されたユーザーにBackend APIに対する認可を与える
- Backend APIはユーザーが認可されているかを検証
ログイン時にJWT形式のアクセストークンを取得し、Backend APIで検証するという手順になります。
2020/4/28 追記:
Backend APIの扱いについて、わかりにくかったので追記。
本記事に登場するシステムをOAuthでの役割に当てはめると以下のようになります。
登場人物 | OAuthでの役割 |
---|---|
Single Page Application | クライアント |
Backend API | リソースサーバー |
Auth0 | 認可サーバー |
Auth0には自前で用意したBackend APIをリソースサーバーとして登録しておき、そのリソースサーバーに対する認可をクライアントとなるアプリにアクセストークンという形で与えることができるので、本記事はそれを試しています。
アプリの準備
SPAの準備
0から始めるので、とりあえずサンプルアプリそのままでやってみます。
- Auth0にログインしてDashBoardを開く
-
DashBoard > Applications > CREATE APPLICATIONボタン
を押してSingle Page Applicationを登録 -
DashBoard > Applications > 2.で登録したSPAを選択 > Quick Start
の中から使用するライブラリを選択(今回自分はライブラリを使用しない素のjavascriptを選んだので、以降はその前提での作業手順になります。) - 3.で選択したもののQuick Startが表示されるので、その画面内のDOWNLOAD SAMPLEボタンを押下してサンプルアプリをダウンロードします。
- サンプルアプリをダウンロードする時に表示された説明に従って、
DashBoard > Applications > Settings
のAllowed Callback URLs
、「Allowed Web Origins」
、Allowed Logout URLs
を設定する。 - ダウンロードしたサンプルアプリを
npm install
後にnpm start
で立ち上げる。
先にApplicationを登録してからサンプルアプリをダウンロードすると、client_id等の諸々の設定を済んだ状態のアプリがダウンロードできるので、上記手順だけであっという間にアプリを立ち上げることができます。
※余談
SPAなので認証フローはimplicitなのかなと思っていましたが、
Auth0の新しいSPA用のSDKのソースを見た感じ、PKCEでやっているようです。
Backend APIの準備
こちらも同様にサンプルアプリをそのまま使います。
- Auth0にログインしてDashBoardを開く
-
DashBoard > APIs > CREATE APIボタン
を押してAPIを登録 - APIはDashBoardからサンプルアプリをダウンロードできないので、Auth0のドキュメントのQuick Startを開き、任意の言語、フレームワークを選択。(自分は
Node(Express)APIを選択。以降の説明はこれを前提にします。) - 3.で選択したもののQuick Startが表示されるので、その画面内のDOWNLOAD SAMPLEボタンを押下。
- 表示されたDownload sample画面でAPIを選択できるので、2.で登録したAPIを選択し、サンプルアプリをダウンロードする。
- ダウンロードしたサンプルアプリを
npm install
後にnpm start
で立ち上げる。
これだけで、以下の3つのエンドポイントを持ったAPIサーバーを立ち上げることができます。
app.get('/api/public', function(req, res) {
res.json({
message:
"Hello from a public endpoint! You don't need to be authenticated to see this."
});
});
app.get('/api/private', checkJwt, function(req, res) {
res.json({
message:
'Hello from a private endpoint! You need to be authenticated to see this.'
});
});
const checkScopes = jwtAuthz(['read:messages']);
app.get('/api/private-scoped', checkJwt, checkScopes, function(req, res) {
res.json({
message:
'Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.'
});
});
-
/api/public
: 認可不要の誰でもリクエスト可能なAPI。 -
/api/private
: スコープなしのアクセストークンを含むリクエストで利用可能なAPI。checkJwt
ミドルウェアでJWT形式のアクセストークンの検証を実施。 -
/api/private-scoped
: スコープあり(サンプルアプリの場合:read:messages
)のアクセストークンを含むリクエストで利用可能なAPI。checkJwt
でのアクセストークンの検証に加え、checkScopes
でscopeの検証も実施。
JWT形式のアクセストークンについては後述。
Backend APIのDashBoard設定
サンプルアプリに定義されているスコープread:messages
をDashBoardからAPIのPermissionsに設定します。
-
DashBoard > APIs > 今回作成したAPIを選択 > Permissions
を選択 - Permissionに
read:messages
、Descriptionに適当な説明を設定し、ADDボタンを押下
DashBoard > APIs > 今回作成したAPIを選択 > Machine to Machine Applications
で、Backend APIと連携するアプリの紐付けをする必要があるかと思いましたが、説明に「SPAやNative Appは追加の設定は不要」的なことが書いてあるのに加え、SPAはアプリの一覧にも出てきませんでした。どうやら設定は不要のようです。
ユーザーの作成とロール付与
SPAとBackend APIを利用するユーザーを作成します。
-
DashBoard > Users & Roles > Users > CREATE USERボタン
を押下し、ユーザーを作成
次に、ロールの作成とユーザーに対してロールの付与をします。
これをやらないと/api/private-scoped
に対する認可(scope)を要求することができません。
-
DashBoard > Users & Roles > Roles > CRETE ROLEボタン
を押下し、適当なNameとDescriptionを入力して新しいロールを作成。 -
DashBoard > Users & Roles > Roles > 登録したロールを選択 > Permissions > ADD PERMISSIONSボタン
を押下し、先に登録したPermissionread:messages
を選択する。 -
DashBoard > Users & Roles > Roles > 登録したロールを選択 > Users > ADD USERSボタン
を押下し、ロールを付与するユーザーを選択する。(Userの詳細ページからロールを付与することも可能です。)
Backend APIを利用できるようにSPAを修正
SPAからAuth0にBackend APIに対する認可を要求
SPAのサンプルアプリそのまんまだと当然Backend APIに対する認可を取得できていないので、ログイン時にAuth0に認可を要求するようにSPAを修正します。
const configureClient = async () => {
const response = await fetchAuthConfig();
const config = await response.json();
auth0 = await createAuth0Client({
domain: config.domain,
client_id: config.clientId,
audience: "xxxxxxxxxxxx", // Backend APIのIdentifierを設定
scope: "read:messages" // APIに定義したPermissionを設定
});
};
通常、Auth0が発行するアクセストークンの形式は無意味文字列ですが
ログイン時のパラメータにaudience
を設定をすると、JWT形式のアクセストークンが取得できます。
JWT形式のアクセストークンが取得できているかを確認するためのコードも仕込んでみます。
window.onload = async () => {
await configureClient();
// 省略
if (shouldParseResult) {
console.log("> Parsing redirect");
try {
const result = await auth0.handleRedirectCallback();
if (result.appState && result.appState.targetUrl) {
showContentFromUrl(result.appState.targetUrl);
}
console.log("Logged in!");
const accessToken = await auth0.getTokenSilently(); // アクセストークンを取得
console.log(accessToken); // アクセストークンを表示
} catch (err) {
console.log("Error parsing redirect:", err);
}
window.history.replaceState({}, document.title, "/");
}
updateUI();
};
ログイン時にconsoleにアクセストークンが表示されるようになります。
eyJ0eBX2K-cZ2F ... em0dB84Zo5fiEKq6_fQ
これをjwt.ioでデコードして、アクセストークンのペイロードをみてみると、audience
とscope
に要求した内容が含まれていることがわかります。
{
"iss": "https://{ドメイン名}/",
"sub": "auth0|5d ... 7f",
"aud": [
"xxxxxxxxxxxx",
"https://{ドメイン名}/userinfo"
],
"iat": 1569000000,
"exp": 1569000000,
"azp": "8R ... lD",
"scope": "openid profile email read:messages"
}
APIリクエストボタンを設置
SPA上に3つのAPIを呼び出すボタンを置きます。
動作確認がしたいだけなのでココは本当にテキトーです。
<div>
<button class="btn btn-primary" id="publicApiRequestBtn" onclick="publicApiRequest()">
Public API Request
</button>
<button class="btn btn-primary" id="privateApiRequestBtn" onclick="privateApiRequest()">
Private API Request
</button>
<button class="btn btn-primary" id="privateScopedApiRequestBtn" onclick="privateScopedApiRequest()">
Private-Scoped API Request
</button>
</div>
// /api/publicへのリクエスト
const publicApiRequest = async () => {
const myHeaders = new Headers();
const accessToken = await auth0.getTokenSilently();
myHeaders.set("Authorization", "Bearer " + accessToken);
const response = await fetch("http://{ホスト名}/api/public", {
method: "GET",
headers: myHeaders
})
.then((res) => {
return res.json();
})
.catch((err) => {
console.log(err);
});
alert(JSON.stringify(response));
};
// /api/privateへのリクエスト
const privateApiRequest = async () => {
const myHeaders = new Headers();
const accessToken = await auth0.getTokenSilently();
myHeaders.set("Authorization", "Bearer " + accessToken);
const response = await fetch("http://{ホスト名}/api/private", {
method: "GET",
headers: myHeaders
})
.then((res) => {
return res.json();
})
.catch((err) => {
console.log(err);
});
alert(JSON.stringify(response));
};
// /api/private-scopeへのリクエスト
const privateScopedApiRequest = async () => {
const myHeaders = new Headers();
const accessToken = await auth0.getTokenSilently();
myHeaders.set("Authorization", "Bearer " + accessToken);
const response = await fetch("http://{ホスト名}/api/private-scoped", {
method: "GET",
headers: myHeaders
})
.then((res) => {
return res.json();
})
.catch((err) => {
console.log(err);
});
alert(JSON.stringify(response));
};
APIリクエストを実行
ここまでの設定がうまくできていれば、SPAに配置したボタンを押すとそれぞれのAPIのレスポンスがアラート表示されるはずです。
{"message":"Hello from a public endpoint! You don't need to be authenticated to see this."}
{"message":"Hello from a private endpoint! You need to be authenticated to see this."}
{"message":"Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this."}
APIリクエスト時にCORSエラーが出る場合は、Backend API側のCORSオプションを見直してください。