Auth0のサンプルアプリケーション(JS)で Google ログインした場合にトークンが取得できない問題の回避

2019.11.14

テナント作成したてでは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();
};

このコードでは、期待どおりに トークンを取得することができませんでした。

no_token.png

ログイン直後にトークンを取得すれば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();
};

この状態でログインします。

token_get.png

意図どおりトークンが取得できました。暗黙の認証を経由していないログイン直後にトークンを取得することで、Developer Keys の制限を回避しました。

まとめ

Developer Keys の制限を把握していなかったことによる問題の回避方法でした。テナントを作成した直後はいろいろ試したくなりますが、Developer Keys の制限を把握しておくことでよりスムースに検証を進められると思います。