🦔

【Next.js】Auth0で認証が必要な画面の作り方

2020/12/07に公開

Next.jsでAuth0を実装してアプリを作るとログイン状態でしか見せたくないページもあるでしょう。たとえば、ユーザーのプロフィールを設定する画面に直接URLを入力したときとか。ログアウト状態だと遷移せずにログイン画面にリダイレクトしたいと考えますよね。

実際、zennでもログアウト状態でhttps://zenn.dev/settings/profileにアクセスしてプロフィール編集画面を開こうとするとログイン画面が表示されるようになっています。

なので、今回はログアウト状態で認証が必要な画面に遷移したときに、ログイン画面にリダイレクトする方法について解説します。

Next.jsでAuth0を設定する方法についてはこちらでまとめています。

クライアント側で認証が必要な場合

nextjs-auth0のgithubのサンプルに載っている/components/with-auth.jsxを使えばログアウト状態で遷移すればログイン画面にリダイレクトされます。以下のソースではログアウト状態で遷移するとログイン画面にリダイレクトする画面を作っています。

pages/protected-page.jsx(ログイン状態でしか見せない画面)

import React from 'react';

import Layout from '../components/layout';
import { useFetchUser } from '../lib/user';
import withAuth from '../components/with-auth';

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

  return (
    <Layout user={user} loading={loading}>
      <h1>Protected Page</h1>

      {loading && <p>Loading profile...</p>}

      {!loading && user && (
        <>
          <p>Profile:</p>
          <pre>{JSON.stringify(user, null, 2)}</pre>
        </>
      )}
    </Layout>
  );
}

//高次コンポーネントを使ってログイン画面にリダイレクト
export default withAuth(ProtectedPage);

/components/with-auth.jsx(ログアウト状態だとリダイレクトするコンポーネント)

import React, { Component } from 'react';

import auth0 from '../lib/auth0';
import { fetchUser } from '../lib/user';
import createLoginUrl from '../lib/url-helper';
import RedirectToLogin from '../components/login-redirect';

export default function withAuth(InnerComponent) {
  return class Authenticated extends Component {
    static async getInitialProps(ctx) {

   //api/meにアクセスしてユーザー情報を取得
      if (!ctx.req) {
        const user = await fetchUser();
        return {
          user
        };
      }

   //セッションのユーザー情報を取得
      const session = await auth0.getSession(ctx.req);
      if (!session || !session.user) {
        ctx.res.writeHead(302, {
          Location: createLoginUrl(ctx.req.url)
        });
        ctx.res.end();
        return;
      }
      
      //ユーザー情報をコンポーネントの引数(this.props)に渡す
      return { user: session.user };
    }

    constructor(props) {
      super(props);
    }

    render() {
      //ユーザー情報が存在しなければログイン画面にリダイレクト
      if (!this.props.user) {
        return <RedirectToLogin />;
      }

      return <div>{<InnerComponent {...this.props} user={this.props.user} />}</div>;
    }
  };
}

protected-page.jsxでは、高次コンポーネントを使って認証機能を追加しています。

高次コンポーネントとは、コンポーネントを受け取り、コンポーネントを生成する関数のことです。Protected-pageのコンポーネントをwithAuthに渡すことで認証機能を追加しています。

with-auth.jsxではサーバーサイドレンダリングした際に、以下のどちらかの処理によってユーザー情報を取得しています。

  • /api/meにアクセスした際に取得できるユーザー情報
  • セッションに格納されているユーザー情報

どちらもログアウト状態ではユーザー情報は取得できないようになっており、そのユーザー情報をコンポーネントの引数(this.props)に渡しています。
そして、渡された引数にユーザー情報が存在しなければrenderメソッドでログイン画面にリダイレクトしているのです。RedirectToLoginタグの部分が該当箇所になります。

/components/login-redirect.jsx

import Router from 'next/router';
import React, { Component } from 'react';

import Layout from '../components/layout';
import createLoginUrl from '../lib/url-helper';

export default class RedirectToLogin extends Component {
  componentDidMount() {
    //マウントされたときにログイン画面をロード
    window.location.assign(createLoginUrl(Router.pathname));
  }

  render() {
    return (
      <Layout>
        <div>Signing you in...</div>
      </Layout>
    );
  }
}

login-redirect.jsxではマウントされたときに、window.location.assignでログイン画面を表示しているだけです。

これにより、認証が必要な状態で遷移するとログイン画面にリダイレクトされるわけです。

サーバーサイドでログイン画面で認証が必要な場合

認証はページだけでなく、Web APIでも必要な場合があるでしょう。そんな時に使えるのがrequireAuthenticationです。

以下のソースでは、認証が必要なWeb APIであるcallを作成しています。http://localhost:3000/api/callでアクセス可能です。

page/api/call.jsx

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

export default auth0.requireAuthentication(async (req, res) => {
  res.json({
    call: '0000-000-000'
  });
});

ログイン状態でアクセスすればcallの値を取得できますが、ログアウト状態でアクセスすると「認証できません」のエラーが返されています。

ログイン状態

ログアウト状態

そのため、以下のようにクリックハンドラーでcallAPIを呼び出すとログイン状態だとWeb APIから渡された値を取得できますが、ログアウト状態だとundefinedになります。

page/index.jsx

import React from 'react';

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

export default function Home(props) {
  const { user, loading } = useFetchUser();
  
  //callAPIから値を取得
  const fetchCall = async () => {
    const response = await fetch("http://localhost:3000/api/call");
    const data = await response.json();
    alert("call:" + data.call)
  }

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

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

      {/* ログアウト状態 */}
      {!loading && !user && (
        <>
          {/* 取得できない */}
          <button onClick={fetchCall}>ログアウト状態</button>
        </>
      )}

      {/* ログイン状態 */}
      {user && (
        <>
          {/* 取得できる */}
          <button onClick={fetchCall}>ログイン状態</button>
        </>
      )}
    </Layout>
  );
}

ログイン状態

ログアウト状態

まとめ

  • クライアント側で認証が必要な場合、with-authを使えばログイン画面にリダイレクトされる
  • サーバーサイドで認証が必要な時はrequireAuthenticationを使えば認証されていない場合、エラーとなる

Discussion