Auth0のサンプルアプリケーション(JS)で Google ログインした場合にトークンが取得できない問題の回避
テナント作成したてではGoogleログインで Silent Auth できない
Auth0でテナントを作成すると、Connections > Social で Goole ログインが利用可能な状態になっています。本来 Google ログインを利用するためには、開発者ないし所属組織が所有する Google アカウントで 認証のための ClientID / Secret を発行する必要があります。Auth0 はその機能をいちはやく試してもらうために、Developer Keys というものを用意してくれていて、これによりサンプルアプリケーションですぐに Google ログインを試すことが可能です。
ただし、この Developer Keys を利用した Google ログインには制限があり、そのひとつに 暗黙の認証は利用できない(失敗する) というものがありました。
Test Social Connections with Auth0 Developer Keys
prompt=none won't work on the /authorize endpoint. Auth0.js' checkSession() method uses prompt=none internally, so that won't work either.
今回、サンプルアプリケーションで、この制限を把握せずにトークンの取得を試みたため失敗する状況になりました。その回避方法を記録します。なお、この事象は次の場合には遭遇しません:
- 独自のGoogleアカウントにて ClientID/Secret を発行、Auth0に登録してログインした場合
- サンプルアプリケーションにおいて、Google ログインではなく メールアドレス、パスワードでサインアップ・ログインした場合
サンプルアプリケーションはこちらを利用しました。
または、Auth0のダッシュボードで、 Applications > 新規作成 > Quick Start > JS > Download Sample
という手順で同じものが手に入ります。
アクセストークン取得に失敗する
サンプルアプリケーションの app.js を以下のように修正してアクセストークンの取得を試みました。なお、トークン取得の目的が RBAC( Role-Based Access Control )の確認でした。RBACで設定した Permission がトークンに含まれているかどうかをみたかったので、 audience
の設定も行っています。
より詳細な Role-Based Access Control の情報はこちらのブログを参照ください:
// The Auth0 client, initialized in configureClient() let auth0 = null; /** * Starts the authentication flow */ const login = async (targetUrl) => { try { console.log("Logging in", targetUrl); const options = { redirect_uri: window.location.origin, audience: 'https://example.jp' }; if (targetUrl) { options.appState = { targetUrl }; } await auth0.loginWithRedirect(options); } catch (err) { console.log("Log in failed", err); } }; /** * Executes the logout flow */ const logout = () => { try { console.log("Logging out"); auth0.logout({ returnTo: window.location.origin }); } catch (err) { console.log("Log out failed", err); } }; /** * Retrieves the auth configuration from the server */ const fetchAuthConfig = () => fetch("/auth_config.json"); /** * Initializes the Auth0 client */ const configureClient = async () => { const response = await fetchAuthConfig(); const config = await response.json(); auth0 = await createAuth0Client({ domain: config.domain, client_id: config.clientId }); }; /** * Checks to see if the user is authenticated. If so, `fn` is executed. Otherwise, the user * is prompted to log in * @param {*} fn The function to execute if the user is logged in */ const requireAuth = async (fn, targetUrl) => { const isAuthenticated = await auth0.isAuthenticated(); if (isAuthenticated) { return fn(); } return login(targetUrl); }; // Will run when page finishes loading window.onload = async () => { await configureClient(); // If unable to parse the history hash, default to the root URL if (!showContentFromUrl(window.location.pathname)) { showContentFromUrl("/"); window.history.replaceState({ url: "/" }, {}, "/"); } const bodyElement = document.getElementsByTagName("body")[0]; // Listen out for clicks on any hyperlink that navigates to a #/ URL bodyElement.addEventListener("click", (e) => { if (isRouteLink(e.target)) { const url = e.target.getAttribute("href"); if (showContentFromUrl(url)) { e.preventDefault(); window.history.pushState({ url }, {}, url); } } }); const isAuthenticated = await auth0.isAuthenticated(); if (isAuthenticated) { console.log("> User is authenticated"); const token = await auth0.getTokenSilently({ audience: 'https://example.jp' }); console.log(token); window.history.replaceState({}, document.title, window.location.pathname); updateUI(); return; } console.log("> User not authenticated"); const query = window.location.search; const shouldParseResult = query.includes("code=") && query.includes("state="); 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!"); } catch (err) { console.log("Error parsing redirect:", err); } window.history.replaceState({}, document.title, "/"); } updateUI(); };
このコードでは、期待どおりに トークンを取得することができませんでした。
ログイン直後にトークンを取得すればOK
今回は検証目的でトークン取得さえできれば良いので、Silent Auth が走る前、つまりログイン直後にトークン取得処理を移動させました。
// The Auth0 client, initialized in configureClient() let auth0 = null; /** * Starts the authentication flow */ const login = async (targetUrl) => { try { console.log("Logging in", targetUrl); const options = { redirect_uri: window.location.origin, audience: 'https://example.jp' }; if (targetUrl) { options.appState = { targetUrl }; } await auth0.loginWithRedirect(options); } catch (err) { console.log("Log in failed", err); } }; /** * Executes the logout flow */ const logout = () => { try { console.log("Logging out"); auth0.logout({ returnTo: window.location.origin }); } catch (err) { console.log("Log out failed", err); } }; /** * Retrieves the auth configuration from the server */ const fetchAuthConfig = () => fetch("/auth_config.json"); /** * Initializes the Auth0 client */ const configureClient = async () => { const response = await fetchAuthConfig(); const config = await response.json(); auth0 = await createAuth0Client({ domain: config.domain, client_id: config.clientId }); }; /** * Checks to see if the user is authenticated. If so, `fn` is executed. Otherwise, the user * is prompted to log in * @param {*} fn The function to execute if the user is logged in */ const requireAuth = async (fn, targetUrl) => { const isAuthenticated = await auth0.isAuthenticated(); if (isAuthenticated) { return fn(); } return login(targetUrl); }; // Will run when page finishes loading window.onload = async () => { await configureClient(); // If unable to parse the history hash, default to the root URL if (!showContentFromUrl(window.location.pathname)) { showContentFromUrl("/"); window.history.replaceState({ url: "/" }, {}, "/"); } const bodyElement = document.getElementsByTagName("body")[0]; // Listen out for clicks on any hyperlink that navigates to a #/ URL bodyElement.addEventListener("click", (e) => { if (isRouteLink(e.target)) { const url = e.target.getAttribute("href"); if (showContentFromUrl(url)) { e.preventDefault(); window.history.pushState({ url }, {}, url); } } }); const isAuthenticated = await auth0.isAuthenticated(); if (isAuthenticated) { console.log("> User is authenticated"); window.history.replaceState({}, document.title, window.location.pathname); updateUI(); return; } console.log("> User not authenticated"); const query = window.location.search; const shouldParseResult = query.includes("code=") && query.includes("state="); 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 token = await auth0.getTokenSilently({ audience: 'https://example.jp' }); console.log(token); } catch (err) { console.log("Error parsing redirect:", err); } window.history.replaceState({}, document.title, "/"); } updateUI(); };
この状態でログインします。
意図どおりトークンが取得できました。暗黙の認証を経由していないログイン直後にトークンを取得することで、Developer Keys の制限を回避しました。
まとめ
Developer Keys の制限を把握していなかったことによる問題の回避方法でした。テナントを作成した直後はいろいろ試したくなりますが、Developer Keys の制限を把握しておくことでよりスムースに検証を進められると思います。