🙌

【Next.js・Typescript】NexAuthを使ってログイン認証をする

2022/02/17に公開約6,300字

Zennは初体験、ReactもNext.jsもまだ経験1ヶ月、NextAuthはもちろん手探り・・ 
ほぼ自分のためのメモです・・用語などおかしな点があるかとおもいます・・

目的

NextAuthを使って、以下のような普通のログイン認証画面を実装したい。

環境

  • Next.js
  • ユーザ情報はDB参照(APIで取得)
  • Material-UI(MUI)
  • React Hook Form

Session Providerの設定

NextAuth.jsではSessionにユーザ情報を保存する。
Sessionの内容をアプリケーションから確認できるようにするには、SessionProviderの設定が必要となる。

pages/_app.tsx にSessionProviderを設定する

_app.tsx
import { SessionProvider } from 'next-auth/react';

function MyApp({ Component, pageProps: { session, ...pageProps } }) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  );
}

export default MyApp;

useSessionでSessionにアクセスする

NextAuthの「useSession」にて、コンポーネントからsessionへのアクセスが可能となる。
以下がその例

ExampleComponent.tsx
const ExampleComponent = () => {
  const { data: session } = useSession();
  return (
    <div>
      { session && (
        ・・・
      )}
    </div>
  );

NextAuthの設定

[...nextauth].ts

pages/api/auth配下に、[...nextauth].tsファイルを作成し、next-authの設定をしていく

[...nextauth].ts
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { postSigninUser } from "../../../api/loginuser";

export default NextAuth({
  providers: [
    CredentialsProvider({
      id: "credentials",
      name: "credentials",
      credentials: {
        usercode: {
          label: "User ID",
          type: "text",
          placeholder: "User ID",
        },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials, req) => {
        const postData = {
          user_code: credentials?.usercode,
          password: credentials?.password,
        };

	// APIのpostにて、ユーザーテーブルからログインユーザデータを取得してくる
	// postSigninUserメソッドは別途定義している(axiosを使用)
        const res = await postSigninUser(postData);

        if (res.message === "no data") {
          // throw new Error(res);
          return null;
        } else {
          const user = {
            name: res.data[0].shimei,
            email: res.data[0].user_code,
          };
          return user;
        }
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user, account }) {
      // 最初のサインイン
      if (account && user) {
        return {
          ...token,
          accessToken: user.token,
          refreshToken: user.refreshToken,
        };
      }

      return token;
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      session.refreshToken = token.refreshToken;
      session.accessTokenExpires = token.accessTokenExpires;

      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
  // サインイン・サインアウトで飛ぶカスタムログインページを指定
  // サインアウト時に、”Are you sure you want to sign out?”と聞かれるページを挟むのをスキップする
  pages: {
    signIn: "/login",
    signOut: "/login",
  },
  // Enable debug messages in the console if you are having problems
  debug: process.env.NODE_ENV === "development",
});

環境変数(開発環境では.env)に以下を設定

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=XXXX

XXXの部分は、以下コマンドで出てくるキーを記述する

$ openssl rand -base64 32

カスタムログイン画面

react-hook-formを利用、classNameでcssファイルを使ったstyle設定は削っています。

login.tsx
import { useState } from "react";
import { Button } from "@mui/material";
import { ReactElement } from "react";
import { getCsrfToken, signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { CtxOrReq } from "next-auth/client/_utils";
import { useForm } from "react-hook-form";
var md5 = require("md5");

// POSTリクエスト(サインイン・サインアウトなど)に必要なCSRFトークンを返却する
export const getServerSideProps = async (context: CtxOrReq | undefined) => {
  return {
    props: {
      title: "login",
      csrfToken: await getCsrfToken(context),
    },
  };
};

interface IFormValues {
  user_code?: string;
  password?: string;
}

const Login = ({ csrfToken }: { csrfToken: string | undefined }) => {
  const router = useRouter();
  const [error, setError] = useState("");
  const { register, handleSubmit } = useForm<IFormValues>();
  const signInUser = async (data: IFormValues) => {
  // ここで<any>を書かないとtypeエラーが消えなかったので書いています
    await signIn<any>("credentials", {
      redirect: false,
      usercode: data.user_code,
      password: data.password,
      callbackUrl: `${window.location.origin}`,
    }).then((res) => {
      if (res?.error) {
        setError("UserId,Passwordを正しく入力してください");
      } else {
      // ログイン後に飛ぶページ
        router.push("/Xxxxxx");
      }
    });
  };

  return (
    <div style={{ textAlign: "center" }}>
      <form onSubmit={handleSubmit(signInUser)}>
        <input name="csrfToken" type="hidden" defaultValue={csrfToken} />
        <div style={{ marginTop: "15px" }}>
          <input
            type="text"
            placeholder="User ID"
            {...register("user_code")}
          ></input>
        </div>
        <div>
          <label htmlFor="password"></label>
          <input
            type="password"
            placeholder="Password"
            {...register("password", {
              setValueAs: (value) => md5(value),
            })}
          ></input>
        </div>
        <p>
          <span style={{ color: "red" }}>{error}</span>
        </p>
        <div>
          <Button
            variant="contained"
            sx={{
              width: "100%",
            }}
            type="submit"
          >
            ログイン
          </Button>
        </div>
      </form>
    </div>
  );
};

export default Login;

session ログインしていない場合

ログイン画面に飛ばす。
_app.tsxのuseEffectに記述する。

_app.tsx
function MyApp({
  Component,
  pageProps: { session, ...pageProps },
  router,
}: AppPropsWithLayout) {
  useEffect(() => {
    // ここに全ページ共通で行う処理
    router.push("/login");
  }, []);

ハマったこと

実装中にハマったこと、実装中のメモをスクラップに書いています。

https://zenn.dev/furai_mountain/scraps/225734efc9ab7d

参考にさせていただいたサイト

https://zenn.dev/okumura_daiki/articles/c9e0065716d862

https://youtu.be/im8o328q6EI
これ見ながらやればひととおり実装できる

https://cloudcoders.xyz/blog/nextauth-credentials-provider-with-external-api-and-login-page/
英語だけど翻訳すればとてもわかり易い
結局このサイトをかなり参考にした

https://reffect.co.jp/react/next-auth#Session_Provider
SessionProvider、session,callbacksなどの説明が詳しい

Discussion

ログインするとコメントできます