この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
テナント作成したてでは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 の情報はこちらのブログを参照ください:
public/js/app.js
// 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 が走る前、つまりログイン直後にトークン取得処理を移動させました。
public/js/app.js
// 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 の制限を把握しておくことでよりスムースに検証を進められると思います。