NextAuth.jsについて調 べたので使 い方 まとめとく
設定
アプリGitHubとGoogleでのログインを
開発 環境 準備
docker compose
で
version: "3"
services:
app:
build:
context: .
container_name: nextauth-example-app
ports:
- "3000:3000"
tty: true
volumes:
- ./app:/app
working_dir: /app
FROM node:18-slim
RUN apt-get update
RUN apt-get install -y openssl
アプリ
$ docker compose up
コンテナにログインして、Next.jsをインストール
$ docker exec -it nextauth-example-app bash
$ npx create-next-app . --ts --use-npm
$ npm run dev
NextAuth.js をインストール
$ npm install --save next-auth
使 う
データベースなしでアプリではユーザー.env
に
NEXTAUTH_SECRET=xxxxxxxxxx
GITHUB_ID=xxxxxxxxxxxxxxxxxxxx
GITHUB_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
.envの
namespace NodeJS {
interface ProcessEnv extends NodeJS.ProcessEnv {
GITHUB_ID: string;
GITHUB_SECRET: string;
}
}
{
・・・
"include": [
"types/**/*.ts",
・・・
],
}
NextAuthの
import NextAuth, { NextAuthOptions } from "next-auth";
import GithubProvider from "next-auth/providers/github";
export const authOptions: NextAuthOptions = {
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
session: { strategy: "jwt" },
}
export default NextAuth(authOptions);
SessionProvider
を
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { Session } from "next-auth";
import { SessionProvider } from "next-auth/react";
export default function App({ Component, pageProps }: AppProps<{ session: Session }>) {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
);
}
GitHubでの
ログインuseSession
のsignIn()
をsignIn("github")
のようにプロバイダーを
import { useSession, signIn, signOut } from "next-auth/react";
export default function Home() {
const { data: session } = useSession();
return session ? (
<>
{JSON.stringify(session)}
<button onClick={() => signOut()}>SignOut</button>
</>
) : (
<>
<button onClick={() => signIn()}>SignIn</button>
</>
);
}
また、デフォルトのSessionは
export interface DefaultSession {
user?: {
name?: string | null
email?: string | null
image?: string | null
}
expires: ISODateString
}
export interface Session extends DefaultSession {}
使 う
アクセストークンをログイン
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
accessToken?: string;
} & DefaultSession["user"];
}
}
デフォルトの[...nextauth].ts
のcallbacks
でデフォルトのsession
コールバックで
callbacks: {
・・・
async session({ session, token }) {
session.user.accessToken = token.accessToken;
return session;
},
},
ただし、session
コールバックの
import { JWT } from "next-auth/jwt";
・・・
declare module "next-auth/jwt" {
interface JWT {
accessToken?: string;
}
}
また、デフォルトのJWTは
export interface DefaultJWT extends Record<string, unknown> {
name?: string | null
email?: string | null
picture?: string | null
sub?: string
}
export interface JWT extends Record<string, unknown>, DefaultJWT {}
セッション コールバックは、セッションがチェックされるたびに
呼 び出 されます。デフォルトでは、セキュリティを高 めるためにトークンのサブセットのみが返 されます。もしあなたがjwt()コールバックを通 してトークンに追加 したもの(上記 のaccess_tokenやuser.idなど)を利用 可能 にしたい場合 は、明示 的 にここに転送 してクライアントが利用 できるようにしなければなりません。
引数 user、account、profile、isNewUserは、ユーザーがサインインした後 、新 しいセッションでこのコールバックが最初 に呼 び出 されたときのみ渡 されます。それ以降 の呼 び出 しでは、tokenのみが使用 可能 です。
callbacks: {
・・・
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token
}
return token
}
},
セッションからアクセストークンをscope
を
・・・
export const authOptions: NextAuthOptions = {
providers: [
GithubProvider({
・・・
authorization: {
params: { scope: "repo" },
},
}),
],
・・・
}
export default NextAuth(authOptions);
アクセストークンを
export default function Home() {
const { data: session } = useSession();
const fetchRepos = () => {
if (session?.user.accessToken) {
return;
}
const url = "https://api.github.com/user/repos?per_page=10";
const headers = {
Authorization: "token " + session.user.accessToken,
};
fetch(url, { headers })
.then((res) => res.json())
.then((json) => console.log(json));
}
・・・
}
使 う
データベースありでデータベース
このアプリではUser
とAccount
をVerificationToken
はメールアドレスをSession
はセッションをデータベースで
Prisma
でMariaDB
を
準備
データベースのMariaDBのdocker-compose.yml
をdocker compose up
しなおす
・・・
MARIADB_HOST=nextauth-example-mariadb
MARIADB_USER=user
MARIADB_DATABASE=app
MARIADB_PASSWORD=password
MARIADB_ROOT_PASSWORD=password
DATABASE_URL="mysql://${MARIADB_USER}:${MARIADB_PASSWORD}@${MARIADB_HOST}:3306/${MARIADB_DATABASE}"
version: "3"
services:
・・・
mariadb:
image: mariadb:10.9.3
container_name: nextauth-example-mariadb
restart: always
ports:
- "3306:3306"
env_file:
- app/.env
volumes:
- mariadb:/var/lib/mysql
- ./mariadb:/docker-entrypoint-initdb.d
volumes:
mariadb:
name: nextauth-example-mariadb
GRANT ALL ON *.* TO user;
Next.jsがPrisma
のインストールと
$ docker exec -it nextauth-example-app bash
$ npm install --save next-auth @prisma/client
$ npx prisma init
prisma/schema.prisma
が
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String? @db.Text
refresh_token_expires_in Int?
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime? @map("email_verified")
image String?
accounts Account[]
@@map("users")
}
設定
NextAuthアダプターのPrisma
クライアントのNextAuth.js
のPrisma
アダプターをインストールしておく
$ npx prisma generate
$ npx prisma migrate dev --name init
$ npm install --save @next-auth/prisma-adapter
[...nextauth].ts
にPrisma
アダプターを
・・・
import { PrismaClient } from "@prisma/client";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
const prisma = new PrismaClient();
export const authOptions: NextAuthOptions = {
・・・
adapter: PrismaAdapter(prisma),
}
export default NextAuth(authOptions);
複数 アカウントと連携 する
User
としてGoogleログインもできるようにする。ここでは.env
、types/environment.d.ts
にGitHubとGOOGLE_ID
とGOOGLE_SECRET
を
Googleプロバイダを
import GoogleProvider from "next-auth/providers/google";
・・・
export const authOptions: NextAuthOptions = {
providers: [
・・・
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
・・・
}
export default NextAuth(authOptions);
ログインsignIn
をUser
にAccount
が
import { signIn, ・・・ } from "next-auth/react";
・・・
export default function Home() {
const { data: session } = useSession();
return session ? (
<>
<button onClick={() => signIn("google")}>Link Google</button>
・・・
</>
) : (
・・・
);
}
User
は1レコードのままでAccount
が2レコードになっていることが
アダプターをカスタマイズする
GoogleとGitHubでUser
をemail
にnull
をセットするようにする。
・・・
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { AdapterUser } from "next-auth/adapters";
const prisma = new PrismaClient();
export const prismaAdapter: Adapter = {
...PrismaAdapter(prisma),
// メールアドレスでの検索 不可 とする
getUserByEmail: () => null,
// email = null として User を登録 する
createUser: async (data) => {
const user = await prisma.user.create({ data: { ...data, email: null } });
return user as AdapterUser;
},
};
export const authOptions: NextAuthOptions = {
・・・
adapter: prismaAdapter,
}
export default NextAuth(authOptions);
使 ったSignInとSignUp
ミドルウェアをこのアプリではUser.email
をnull
としたので、ここではSignupページでEmailの
ミドルウェアでページの
import { withAuth } from "next-auth/middleware";
export default withAuth({
callbacks: {
async authorized({ token }) {
if (token?.name && !token?.email) {
return false;
} else {
return true;
}
},
},
});
export const config = { matcher: ["/"] };
Signin/signup
を
callbacks: {
・・・
async redirect({ baseUrl }) {
return `${baseUrl}/signup`;
},
},
signupページを
import { GetServerSideProps } from "next";
import { getSession } from "next-auth/react";
・・・
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context);
if (!session || session?.user.email != null) {
return {
redirect: {
permanent: false,
destination: "/",
},
};
}
return { props: {} };
};
メールを
import { useState } from "react";
import { ・・・, signIn } from "next-auth/react";
・・・
export default function Signup() {
const [email, setEmail] = useState("");
const [signuped, setSignuped] = useState(false);
const onSignup = async () => {
const res = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
if (res.status == 200) {
setSignuped(true);
} else {
alert("error signup");
}
};
return signuped ? (
<>
<h2>Success Signup</h2>
<button onClick={() => signIn()}>Re Signin</button>
</>
) : (
<>
<h2>Signup</h2>
<input placeholder="email" onChange={(e) => setEmail(e.target.value)} />
<button onClick={onSignup}>Signup</button>
</>
);
}
・・・
API
import type { NextApiRequest, NextApiResponse } from "next";
import { unstable_getServerSession } from "next-auth";
import { authOptions, prismaAdapter } from "./auth/[...nextauth]";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await unstable_getServerSession(req, res, authOptions);
if (!session?.user.id) {
return res.status(401).end();
}
let user = await prismaAdapter.getUser(session!.user.id!);
if (!user || user.email) {
return res.status(401).end();
}
await prismaAdapter.updateUser({ ...user, email: req.body["email"] });
res.status(200).json({});
}
認証
メール + パスワードでのCredentials Provider
をCredentials Provider
ではJWTを
他
その
参考 リンク
GitHub
Discussion
1つ補足 ですが,各 プロバイダーにおいて設定 できるようになったらしいです.
@v4.15.0
(2022/10/24)からallowDangerousEmailAccountLinking
というプロパティをこれは,重複 するメールアドレスでSignUpしようとしたとき選 べる設定 で,アダプターをカスタマイズする以降 をまるっと行 ってくれるようです.
名前 にdangerousとついているので少 し躊躇 しますが,実装 が大変 な時 にはこちらを使 うのもありですね.
User
とAccount
をリンクするかを