@auth0/nextjs-auth0 と @auth0/auth0-react の使い分け
モチベーション
Next.jsのフロントエンドアプリケーションにAuth0を使った認証の機能を導入したい。
以下の2つのSDKライブラリがAuth0から公式に提供されている。
初見ではどちらを選択するのが適切かわからなかったので、調べたことをまとめます。
試したこと1
Next.jsを使っているので、Next.jsにより特化していそうな nextjs-auth0 から試してみる。
https://github.com/auth0/nextjs-auth0#getting-started をみながらAuth0側の設定を行う。
Application TypeにSingle Page ApplicationではなくRegular Web Applicationを選択するのがポイント。
ドキュメントどおりに進めると、ログイン機能ができた。
Cookieには多分SDK内部でセットされたappSessionという名前のセッションIDっぽいものがある。
LocalStorageは空でした。
試してみたこと2
nextjs-auth0 でのアクセストークンの取得。
サーバーサイドでのアクセストークンの取得
ドキュメントによると、getAccessTokenという関数が提供されているのでそれを利用してみる。
引数にリクエストオブジェクトとレスポンスオブジェクトを渡す。
以下の通り、アクセストークンが取得できました。
クライアントサイドでのアクセストークンの取得
トークンをクライアントサイドで取得する方法がnextjs-auth0では提供されていないようです。
トークンそのものをCookieに載せてしまえばクライアントでトークンを扱うことはできてしまいますが、issueで「クライアントでトークンを使用しないほうがいいですよ」と言及されていました。
クライアントでトークンを使用するということは、JavaScriptからトークンにアクセスできるようにするということなので、Cookieにトークンを載せたとしてもhttpOnly属性をfalseにする必要があり、セキュリティ的なリスク(XSSによってトークンを盗まれる可能性)があります。
そのため、クライアントからAPIサーバーに対してリクエストするとき、Authorizationヘッダにトークンを乗せる必要がある場合、nextjs-auth0は適さないと理解しました。
試してみたこと3
auth0-reactで同じことをやってみる。
以下を見ながらログイン機能を実装します。
Cookieにはauth0.is.authenticatedというフラグのような値がセットされ、ローカルストレージは空でした。
試してみたこと4
auth0-reactでのアクセストークンの取得。
サーバーサイドでのアクセストークンの取得
このSDKはNext.jsを意識して作られておらず、素のReact SPAでの使用を想定しているため、サーバーサイドでトークンを取得する方法はないです。
クライアントサイドでのアクセストークンの取得
const { getAccessTokenSilently } = useAuth0();
useEffect(() => {
(async () => {
try {
const token = await getAccessTokenSilently()
console.log({ token })
} catch (e) {
console.error(e)
}
})()
}, [getAccessTokenSilently])
上記のコードでトークンを取得することができました。
個人的な結論
クライアントから直接、認証が必要なAPIを呼びたい場合はauth0-reactを使う。
クライアントから認証が必要なAPIを呼ぶ必要がなく、サーバーサイドでのみ認証情報を扱えば済む場合はnextjs-auth0を使う。
Webアプリケーションでは認証が必要なAPIを使用する場面があって、画面遷移せずにそれらのAPIを呼んでUIを変化させたいケースがあると思うのでnextjs-auth0を使える場面ってなかなか少ないんじゃないだろうか。。
ちょっとnextjs-auth0が適するユースケースがあまり思いつかない
てかここにまとめてあった。。
あと、Auth0がNext.jsの認証のパターンについて図を交えて解説してくれているブログ記事があったので、Next.jsとAuth0をあわせて使おうとしてる方は目を通しておくといいかもしれないです。
疑問に思ったこと
auth0-react はCookieにもLocalStorageにもトークン保持してなくて、一体どこにトークンを保存しているのだろうか?
↑にはトークンを直接ブラウザに保存してるって書いてあるように読み取れるけどどこに保存してるかわからなかった...ブラウザのどこ...?auth0-react will store the user's ID Token and Access Token directly in the browser
調べてみたらauth0-reactが依存しているauth0-spa-jsがiframeを介してトークンを取得しているみたいです。
この辺からソースコードを追ってみるとなんとなく流れが分かりそうです。
見えないiframe要素を作って、iframeの中でAuth0のサーバーと通信してメッセージを受信し、それに含まれるレスポンスからアクセストークンを取り出して返してくれているようです。
iframeでのメッセージ受信についてはこちら。
トークンの取得が完了したあとはiframe要素を削除しています。
実際にChromeで確かめてみました。
ログイン状態でページをリロードしたところを録画しています。
iframe要素が現れたと思ったらすぐに消えました。
そして、一度取得されたトークンはインメモリのキャッシュに保存されるようです。
これならサードパーティーの悪意あるスクリプトが紛れ込んでいてもトークンを盗むのは厳しそうですね。
余談
もうAuth0関係ないのですが、自分がAuth0の他に使用したことのあるIDaaSにAWS Cognitoがあります。
Amplifyライブラリを介してトークンを取得するのですが、Amplifyライブラリでは生のトークンをCookieに直接保存します。
そして、Cookieに保存されたトークンはhttpOnly属性がfalseです。
これは、サードパーティーのスクリプトがトークンを盗めることを意味します。
Amplifyライブラリを使用してアプリケーションに認証機能を組み込む場合、開発者は少なからずこのことは理解しておくべきだと感じました。
issueではトークンをhttpOnly: trueで保存しないことについて残念がっている人たちもいるようです。
セキュリティ面ではAuth0に劣るAmplifyですが、良いところもあると思っています。
それは、クライアントサイドでもサーバーサイドでもトークンの取得が可能であることです。
AmplifyライブラリはSSRに対応しています。
クライアントでもサーバーでも同じインターフェースでトークンの取得ができるのがAmplifyの良さだと思っています。
前述の通り、Auth0ではnextjs-auth0を使用した場合はサーバーサイドでのみトークンの取得が可能、auth0-reactを使用した場合はクライアントサイドでのみトークンの取得が可能だったのでユニバーサルにトークンの取得を行うことは現状Auth0のSDKでは不可能である認識です。
このあたりは、セキュリティ的なリスクを承知でAmplifyを使うのか安全性を優先してAuth0を使用するのか開発者が要件に合わせて判断する必要がありそうですね。