🔑

【Next.js】Auth0を使って手軽で安全に認証機能を実装する方法

2020/12/05に公開1

Next.jsでAuth0を使う方法について解説しているページが少ないので、ここにまとめておきます。

Auth0とは?

Auth0とは、認証・認可のサービスをクラウドで提供している会社。いわゆるIDaaS (Identity as a Service)ベンダーと呼ばれるものです。

ライブラリーが豊富で簡単に実装でき、アプリ内で個人情報を保持しないため安全にログイン機能を実装できるのが魅力です。また、ログインできるSNSアカウントの種類も豊富なのが特徴です。

  • Google
  • facebook
  • github
  • twitter
  • amazon
  • yahoo

などでログインできます。

また、フリープランでは以下の制約があるものの個人で利用するには十分です。

  • 7000人までのアクティブユーザー
  • 2種類までのソーシャルログイン

さらに、ReactやVue、Angularなど有名どころのフレームワークのチュートリアルが揃っているので幅広く利用ができます。

Auth0の登録

それでは、next.jsを使ってAuth0を使ってみましょう!
何はともあれAuth0を使うには登録が必要です。以下のサイトで会員登録を行ってください。

Auth0公式サイト

Auth0に必要なIDとパスワードを作成する

次に認証で使うクライアントID・クライアントシークレットを作成していきます。これらの情報がauth0を使う際のIDとパスワードの役割を担います。

まずは、ダッシュボードにアクセスしてください。
そして、左側の一覧の「Applications」を選択し、左上にある「CREATE APPLICATION」ボタンをクリックします。

そうするとポップアップが立ち上がるのでアプリケーションの名前を入力し、「Single Page Web Applications」を選択します。

作成されると先ほどの一覧に追加されるので選択して設定画面へ遷移します。

アプリケーションの設定

設定画面では、以下の項目を設定します。

  • Allowed Callback URLs:http://localhost:3000/api/login/callback
  • Allowed Logout URLs:http://localhost:3000/

  • Allowed Callback URLs:Auth0で認証された後にリダイレクトされるURL
  • Allowed Logout URLs:ログアウト後にリダイレクトされるURL

アプリケーションで使う情報をメモ

また、アプリケーションでauth0を利用する際に必要な情報があるので以下の項目をメモしておきましょう。

  • Domain
  • Client ID:認証のためのID
  • Client Secret:認証のためのパスワード

nextjs-auth0サンプルプログラムの全体像

ここからはnextjs-auth0のサンプルプログラムを使って解説していきます。githubにソースがあるのでgithubからcloneしておくと分かりやすいです。

次の画像はホーム画面からログインボタンを押して、認証を行った後、再度ホーム画面に戻るまでの画面遷移です。

ホーム画面

ログイン画面

認証後のホーム画面

ログインの流れ

auth0.jsの設定

まずは先ほど作成したクライアントIDやクライアントシークレット等でauth0の設定を行います。

page/api/lib/auth0.jsにauth0を使用するための設定を記述する必要がありますが、設定自体は.env.env.templateの名前を変更が必要)の書き換えです。
先ほどメモっておいた設定画面のパラメータを入力します。

page/api/lib/auth0.js

import { initAuth0 } from '@auth0/nextjs-auth0';
import config from './config';

export default initAuth0({
  audience: config.API_AUDIENCE,
  clientId: config.AUTH0_CLIENT_ID,
  clientSecret: config.AUTH0_CLIENT_SECRET,
  scope: config.AUTH0_SCOPE,
  domain: config.AUTH0_DOMAIN,
  redirectUri: config.REDIRECT_URI,
  postLogoutRedirectUri: config.POST_LOGOUT_REDIRECT_URI,
  session: {
    cookieSecret: config.SESSION_COOKIE_SECRET,
    cookieLifetime: config.SESSION_COOKIE_LIFETIME,
    storeIdToken: true,
    storeRefreshToken: true,
    storeAccessToken: true
  }
});

.env

AUTH0_DOMAIN=「ドメイン」
AUTH0_CLIENT_ID=「クライアントIDAUTH0_CLIENT_SECRET=「クライアントシークレット」
REDIRECT_URI=「コールバック後のURLPOST_LOGOUT_REDIRECT_URI=「ログアウト後のURLSESSION_COOKIE_SECRET=32文字以上のランダムな文字列」

SESSION_COOKIE_SECRETはクッキーを暗号化する際に使われる秘密鍵を設定します。文字列作成サイトがあるのでそこで表示される文字列をセットしておけば大丈夫です。

クライアントIDとクライアントシークレットはauth0が利用するアプリを認証するためのIDとパスワードの役割を果たしています。そのため外部に漏らさないように注意が必要です。

サーバーサイドの処理

次にサーバーサイドでの処理を解説していきます。サーバー側では、以下の処理を行っています。

  • ログイン(page/api/login)
  • コールバック(page/api/callback)
  • ログアウト(page/api/logout)
  • プロフィール取得(page/api/me)

page/api/login

import auth0 from '../../lib/auth0';

export default async function login(req, res) {
  try {
    await auth0.handleLogin(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}

ログイン処理ではauth0のhadleLoginメソッドを使ってauth0のログインページにリダイレクトしています。そのため、ホーム画面でログインボタンが押された際は/api/loginにアクセスして、Auth0のログイン認証画面に遷移しました。

page/api/callback

import auth0 from '../../lib/auth0';

export default async function callback(req, res) {
  try {
    await auth0.handleCallback(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}

構成はログイン処理とほぼ同じです。auth0のページで認証後、このAPIにリダイレクトされクッキーを生成します。そのあとは、auth0で設定したREDIRECT_URIにリダイレクトされます。そのため、http://localhost:3000に遷移しました。

page/api/logout

import auth0 from '../../lib/auth0';

export default async function logout(req, res) {
  try {
    await auth0.handleLogout(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}

logout.jsxではhandleLogoutメソッドを使ってログアウト処理を行っています。そのため、/api/logoutにアクセスするとログアウト処理が走るのです。ログアウト後はpostLogoutRedirectUriでセットしたURLに遷移します。今回の場合だとhttp://localhost:3000/です。

page/api/me

import auth0 from '../../lib/auth0';

export default async function me(req, res) {
  try {
    await auth0.handleProfile(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}

最後にプロフィールを取得するAPIです。SNSでログインを行った場合、ユーザー名やアイコンに設定している画像などが表示されます。

クライアントサイドの処理

最後にクライアント側の処理を見ていきましょう。

index.jsx

import React from 'react';

import Layout from '../components/layout';
import { useFetchUser } from '../lib/user';

export default function Home() {
  const { user, loading } = useFetchUser();

  return (
    <Layout user={user} loading={loading}>
      <h1>Next.js and Auth0 Example</h1>

      {/* ロード中 */}
      {loading && <p>Loading login info...</p>}

      {/* ログアウト状態 */}
      {!loading && !user && (
        <>
          <p>
            To test the login click in <i>Login</i>
          </p>
          <p>
            Once you have logged in you should be able to click in <i>Profile</i> and <i>Logout</i>
          </p>
        </>
      )}

      {/* ログイン状態 */}
      {user && (
        <>
          <h4>Rendered user info on the client</h4>
          <pre>{JSON.stringify(user, null, 2)}</pre>
        </>
      )}
    </Layout>
  );
}

index.jsでは、lib配下にあるuser#useFetchUserメソッドを使ってロード情報とユーザー情報を取得しています。これにより、今どんな状態なのかを知ることができます。

  • ロード中:loadingがtrue
  • ログアウト状態:loadingがfalseかつuser情報が存在しない
  • ログイン状態:user情報が存在する

上のソースではロード中の時に「Loading login info...」というメッセージを出し、ログイン状態ではユーザー情報を出力しています。

ちなみにuseFetchUser内では/api/meにアクセスしてプロフィール情報を取得しています。詳しいことはgithubのソースをご覧ください。

ログイン状態の画面

header.jsx

import React from 'react';
import Link from 'next/link';

import { useUser } from '../lib/user';

const Header = () => {
  const { user, loading } = useUser();

  return (
    <header>
      <nav>
        <ul>
          <li>
            <Link href="/">
              <a>Home</a>
            </Link>
          </li>
          <li>
            <Link href="/about">
              <a>About</a>
            </Link>
          </li>
          <li>
            <Link href="/protected-page">
              <a>Protected Page</a>
            </Link>
          </li>
          {!loading &&
            (user ? (
              // ログイン状態
              <>
                <li>
                  <a href="/shows">My TV Shows</a>
                </li>{' '}
                <li>
                  <Link href="/profile">
                    <a>Profile</a>
                  </Link>
                </li>{' '}
                <li>
                  <a href="/profile-ssr">Profile (SSR)</a>
                </li>{' '}
                <li>
                  <a href="/api/logout">Logout</a>
                </li>
              </>
            ) : (
              // ログアウト状態
              <>
                <li>
                  <a href="/api/login">Login</a>
                </li>
              </>
            ))}
        </ul>
      </nav>
    </header>
  );
};

export default Header;

layoutコンポーネント内のheader.jsxでは、ログイン状態・ログアウト状態で表示する項目を変更しています。

ログアウト状態

ログイン状態

ログアウト状態だと\api\loginにアクセスするloginボタンを表示していますが、ログイン状態だと\api\logoutにアクセスするloginボタンを表示しています。

user情報があるかどうかでログインを判定しています。ユーザー情報が無ければログアウト状態。あればログイン状態。

Discussion

Kaito SugimotoKaito Sugimoto

わかりやすい記事ありがとうございます!

二点気になる部分があったのですが、まず .envAUTH0_SCOPE を指定していないように見えます。
おそらく

AUTH0_SCOPE='openid profile'

などと指定してあげる必要がありそうです。(これをしないとソーシャルログインの場合 callback の画面で id_token not present in TokenSet というエラーが発生します。)

もう一つ、こちらは細かいことなのですが、「アプリケーションの設定」のすぐ下の「Allowed Callback URLs」の URL が http://localhost:3000/api/login/callback となってしまっています。(正しくは http://localhost:3000/api/callback だと思います。)