かべブログ

TypeScript, React, Goなどでまったり個人開発。たまに競プロ。

NextAuth.js +TypeScript 素振り

はじめに

こんにちは、かべです。少し前に Firebase を使った認証ページを作ったので、その続編として今度は NextAuth.js を使った認証ページを作っていこうと思います。公式のチュートリアルから飛べる動画でコーディングしながら解説してくれています。英語がかなり聞きやすいのでおすすめです。ただ JavaScript で実装をしていたため、TypeScript に置き換えて作っていきます。

今回は動画内で紹介されているログイン方法の内 GitHub 認証とメールを使った認証について書いていきます。その他のログイン方法についてもこれと大差ありません。

環境

今回の開発は以下の環境で行いました。

  • Ubuntu 20.04.1
  • NextAuth 3.14.8
  • yarn 1.22.5
  • Next 10.1.3
  • React 17.0.2
  • TypeScript 4.0

Next アプリの作成、事前準備

まず、例のごとくアプリを作成していきます。npx create-next-app --example with-typescript ${好きな名前} でさくっと TypeScript の入った Next のアプリを作り、いらない部分を全て消します。完成したらそこへ移動し、yarn add next-auth sqlite3yarn add -D @types/next-auth を実行します。sqlite3 の部分はどんなデータどんなでも良いですが、今回は動画に合わせます。

また、メール認証のために SendGrid のアカウントが必要なので、無ければアカウントを作成します。登録に審査があり1営業日程度かかるらしいので、急ぎの方は注意です。

NextAuth を使用する準備

環境変数の設定

GitHub 連携用の OAuth App を作成します。GitHubSettings / Developer settings / OAuth Apps にアクセスし、New OAuth App からアプリを作成します。作成出来たら Client ID と Client secrets を控えておきましょう。

f:id:okb_okb:20210420225149p:plain
GitHub 連携用の OAuth App

次に、SendGrid の設定を行います。SendGrid のアカウントを作成したら、Email API の中の SMTP Relay を選択します。適当な API Key の名前を決めて Key を作成し、Server、Ports、Username、Password を控えておきましょう。

f:id:okb_okb:20210420225224p:plain
SendGrid の API 作成画面

f:id:okb_okb:20210420225302p:plain
SendGrid の API 作成後

これらの値が取得出来たら、環境変数に設定します。

.env.local

GITHUB_ID='******'
GITHUB_SECRET='******'
EMAIL_SERVER_USER='apikey'
EMAIL_SERVER_PASSWORD='******'
EMAIL_SERVER_HOST='smtp.sendgrid.net'
EMAIL_SERVER_PORT='587'
EMAIL_FROM='***@***'
NEXTAUTH_URL='http://localhost:3000'
DATABASE_URL='sqlite://localhost/:memory:'

/next.config.js

module.exports = {
  env: {
    GITHUB_ID: process.env.GITHUB_ID,
    GITHUB_SECRET: process.env.GITHUB_SECRET,
    AUTH0_CLIENTID: process.env.AUTH0_CLIENTID,
    AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET,
    AUTH0_DOMAIN: process.env.AUTH0_DOMAIN,
    EMAIL_SERVER_USER: process.env.EMAIL_SERVER_USER,
    EMAIL_SERVER_PASSWORD: process.env.EMAIL_SERVER_PASSWORD,
    EMAIL_SERVER_HOST: process.env.EMAIL_SERVER_HOST,
    EMAIL_SERVER_PORT: process.env.EMAIL_SERVER_PORT,
    EMAIL_FROM: process.env.EMAIL_FROM,
    NEXTAUTH_URL: process.env.NEXTAUTH_URL,
    DATABASE_URL: process.env.DATABASE_URL,
  },
};

[...nextauth].ts の作成

ログインの操作をした時に使用する認証の種類やその設定などを記述するファイルを書きます。これは /pages/api/auth/[...nextauth].ts に作成します。

/pages/api/auth/[...nextauth].ts

import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import Providers from "next-auth/providers";

const options = {
  providers: [
    Providers.GitHub({
      clientId: process.env.GITHUB_ID as string,
      clientSecret: process.env.GITHUB_SECRET as string,
    }),
    Providers.Email({
      server: {
        host: process.env.EMAIL_SERVER_HOST as string,
        port: Number(process.env.EMAIL_SERVER_PORT),
        auth: {
          user: process.env.EMAIL_SERVER_USER as string,
          pass: process.env.EMAIL_SERVER_PASSWORD as string,
        },
      },
      from: process.env.EMAIL_FROM,
    }),
  ],
  database: {
    type: "sqlite",
    database: ":memory:",
    synchronize: true,
  },
};

export default (req: NextApiRequest, res: NextApiResponse) =>
  NextAuth(req, res, options);

clientId などの型が stringstring | undefined が認められなかったため楽するために as 使ってますが許してください。今回は GitHub とメールしか使っていませんが、その他の認証を使いたいときは同様に providers の中に好きなものを足せばよいです。

_app.tsx の作成

アプリ全体でセッションを使う準備をします。/pages/_app.tsx にセッションの Provider を用意し、アプリ全体を囲ってあげます。

/pages/_app.tsx

import { AppProps } from "next/app";
import { Provider } from "next-auth/client";

const MyApp = ({ Component, pageProps }: AppProps) => {
  return (
    <Provider session={pageProps.session}>
      <Component {...pageProps} />
    </Provider>
  );
};

export default MyApp;

ここまで出来たら、いよいよアプリの中身を作成していきます。

トップページ

トップページは以下のような機能にします。

  • ログインしていない場合、「ログインしてません」という文とログインボタンを表示
  • ログインしている場合ユーザーの名前かメールを表示し、ログインした人のみ見られるコンテンツへのリンクを載せる。またログアウトボタンも実装。

これを実現するため、NextAuth の signIn, signOut, useSession の3つの機能を使います。useSession<Provider></Provider> 内のセッションを取得できます。

/pages/index.tsx

import { NextPage } from "next";
import Link from "next/link";
import { signIn, signOut, useSession } from "next-auth/client";

const IndexPage: NextPage = () => {
  const [session, loading] = useSession();

  return (
    <>
      {!session && (
        <>
          <div>ログインしていません</div>
          <button onClick={() => signIn()}>ログイン</button>
        </>
      )}
      {session && (
        <>
          こんにちは、{session.user.name ?? session.user.email} さん
          <br />
          <button>
            <Link href="/secret">秘密のページへ</Link>
          </button>
          <button onClick={() => signOut()}>ログアウト</button>
        </>
      )}
    </>
  );
};

export default IndexPage;

Session.user.*** でログイン中のユーザーの情報を取得できます。

ログインした人のみ見られるページ

最後に、ログインした人のみ見られる秘密のページを作ります。まず、ログインしたユーザーのみ叩いて情報を得られる API を作ります。以下はログインしていればステータスコード200 と欲しい情報、ログインしていなければステータスコード 401 を返す API です。TypeScript 用に少し動画と書き方を変えています。

/pages/api/secret/index.ts

import { IncomingMessage, ServerResponse } from "http";
import { getSession } from "next-auth/client";

export default async (req: IncomingMessage, res: ServerResponse) => {
  const session = await getSession({ req });

  if (session) {
    res.statusCode = 200;
    res.setHeader("Content-type", "application/json");
    res.end(
      JSON.stringify({
        content: "秘密のページへようこそ!!",
      })
    );
  } else {
    res.statusCode = 401;
    res.setHeader("Content-type", "application/json");
    res.end(
      JSON.stringify({
        error: "ログインしてください",
      })
    );
  }
};

最後に秘密のページを作ります。これでログインしている時は API からの情報を表示し、ログインしていない時は「ログインしてください」という情報を表示することが出来ます。セッションが変化することで useEffect が走り、情報を取得してきます。セッション管理に使うのは先ほどと同じく useSession です。

import { NextPage } from "next";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/client";

const Secret: NextPage = () => {
  const [session, loading] = useSession();
  const [content, setContent] = useState("");

  useEffect(() => {
    const fetchData = async () => {
      const res = await fetch("/api/secret");
      const json = await res.json();

      if (json.content) {
        setContent(json.content);
      }
    };
    fetchData();
  }, [session]);

  if (typeof window !== "undefined" && loading) return <></>;

  if (!session) {
    return (
      <>
        <h1>ログインしてください</h1>
      </>
    );
  }

  return (
    <>
      <h1>秘密のページ</h1>
      {content}
    </>
  );
};

export default Secret;

これで画像のようなアプリが作れます。

f:id:okb_okb:20210420225441p:plain
トップページ(ログイン前)

f:id:okb_okb:20210420225500p:plain
トップページ(ログイン後)

f:id:okb_okb:20210420225528p:plain
秘密のページ

f:id:okb_okb:20210420225545p:plain
秘密のページ(ログインしていない時)

終わりに

SendGrid や(今回はしてませんが) Auth0 を用いた本格的な認証も簡単に作ることができ、非常に便利でした。Firebase を使った認証も以前やったので、そろそろログインを組み込んで何か作りたい…

今回のコードはこちらに上がっています。

参考