next auth 調査

next authは認証を簡単にしてくれるもの. Google Provider, Github Provider, email認証が存在する.
- authという名前になっているのだが、便宜上next-authとして記載していく
認証のデータについて
- An open-source solution that allows you to keep control of your data
- Supports Bring Your Own Database (BYOD) and can be used with any database
- Built-in support for MySQL, MariaDB, Postgres, SQL Server, MongoDB and SQLite
- Works great with databases from popular hosting providers
- Can also be used without a database (e.g. OAuth + JWT)
認証にはoauthや独自のemail認証が組み込まれており、多種多様な方法が取れる.
ユーザのdatabaseも自前で用意する必要がなく JWT + oauthで認証ができる。もちろん、databaseを用意してoauthで認証できたデータをdbに保存することもできる
Note: Email sign-in requires a database to be configured to store single-use verification tokens.
ただし、email認証で行うときはverificationの確認は必要となる.
セキュア性
- Promotes the use of passwordless sign-in mechanisms
- Designed to be secure by default and encourage best practices for safeguarding user data
- Uses Cross-Site Request Forgery Tokens on POST routes (sign in, sign out)
- Default cookie policy aims for the most restrictive policy appropriate for each cookie
- When JSON Web Tokens are enabled, they are encrypted by default (JWE) with A256GCM
Auto-generates symmetric signing and encryption keys for developer convenience- Features tab/window syncing and keepalive messages to support short-lived sessions
- Attempts to implement the latest guidance published by Open Web Application Security Project
パスワードレスの認証を推奨しており、sign in, sign outするpost apiはcsrfのトークンを載せる.jwtの認証トークンのデフォルト暗号技術はA256GCMを利用しており、Open Web Application Security Projectの最新情報の技術に則る形で実装を試みている

apiのディレクトリパス
// pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"
export const authOptions = {
// Configure one or more authentication providers
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
// ...add more providers here
],
}
export default NextAuth(authOptions)
pages/api/auth/[...nextauth].ts
となり、signin, signoutなどのエンドポイントもここに集約される.app routerの場合pages/app/api/auth/[...nextauth]/router.ts
となる
セッションを使うとき
データ
{
user: {
name: string
email: string
image: string
},
expires: Date // This is the expiry of the session, not any of the tokens within the session
}
データは必要最低限のデータが使えるようになっている
クライアント
useSession
を使うことでuserを取得できる.
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
ただし、
Check out the client documentation to see how you can improve the user experience and page performance by using the NextAuth.js client. If you are using the Next.js App Router, please note that <SessionProvider /> requires a client component and therefore cannot be put inside the root layout. For more details, check out the Next.js documentation.
にある通り、SessionProvider
はnext13以降のapp routerで使う場合は注意する必要がある. SessionProvider
をレイアウトのようなところで書くとuse client
が配下でも適用されることになり、全てがクライエントコンポーネントとして認識されるためである.
signin()
やsignout()
関数が提供されている.それぞれログイン,ログアウトするときに使う.引数にはredirectやcallbackがある. callbackにはログインされた後に飛ばすURL, 何も設定がなければドメインのページに飛ばされる. redirectはログイン後に別のページに飛ばさないようにする設定(email or credential のみ適用可能).
サーバ
getServerSession
を使うことでuserを取得できる.
next13以前ではコンポーネント内にgetServerSession
を記載することは見られなかったことだと思うが、以下のようにすることで記載できる
import { redirect } from 'next/navigation'
import { getServerSession } from 'next-auth'
export default async function LoginPage() {
const user = await getServerSession()
if (user) {
redirect('/')
}
return (
<A />
)
}
今までであればgetServerSideProps
を使って↑のようなものはリダイレクトさせていたがgetServerSideProps
がないので↑のように使うことができる.

REST API
- GET /api/auth/signin
- ログインページに飛ぶ
- next optionの
pages
にて場所は指定可能
- POST /api/auth/signin/:provider
- 指定したproviderでのログイン. この時getリクエストで
/api/auth/csrf
からcsrf tokenを取得しheaderに付与してPOSTしている
- 指定したproviderでのログイン. この時getリクエストで
- GET/POST /api/auth/callback/:provider
- oauthの認証のcallbackされるエンドポイント
- GET /api/auth/signout
- ログアウトページに飛ぶ
- next optionの
pages
にて場所は指定可能
- POST /api/auth/signout
- ログアウト
- GET /api/auth/session
- session情報取得.
getSession()
でAPI呼んでいるのだと思う
- session情報取得.
- GET /api/auth/csrf
- ログイン, ログアウトで必要
- GET /api/auth/providers
- 設定しているproviderの情報. (バレてはいけない情報が書いているわけではないよ)

型
session
とjwt
の型の拡張方法について記載
import NextAuth, { DefaultSession } from "next-auth"
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
user: {
/** The user's postal address. */
address: string
} & DefaultSession["user"]
}
}
import { JWT } from "next-auth/jwt"
declare module "next-auth/jwt" {
/** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
interface JWT {
/** OpenID ID Token */
idToken?: string
}
}
session
では例えば、roleを付与したりする

next-auth 初期設定
basic
// /pages/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth"
const handler = NextAuth({
...
})
export { handler as GET, handler as POST }
REST APIにもあったようにnext-authのエンドポイントは api/auth/〇〇
となっているのでそれを作成してやる. [...nextauth]
この書き方はnext特有の書き方であり, 該当するエンドポイント以下のパスはここにリクエストすることになる
advanced
import type { NextApiRequest, NextApiResponse } from "next"
import NextAuth from "next-auth"
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
// Do whatever you want here, before the request is passed down to `NextAuth`
return await NextAuth(req, res, {
...
})
}
↑のようにNextAuth
のオプションの中にreq
とres
が使えるような書き方が特徴的である. callback
やjwt
などで細工ができる

next-auth options
env
- NEXTAUTH_URL
- callbackされる時に使われたりする.
signin
した時にドメインとなるurl - 使われたりする主な場所
- callbackされる時に使われたりする.
- NEXTAUTH_SECRET
- jwtの暗号化に使われる
- NEXTAUTH_URL_INTERNAL
- NEXTAUTH_URLが正規のurlではない場合に使うとのこと. vercel用なのかな.
options
providers
認証する方式. github
やgoogle
やemail
で認証するかを決定する
secret
jwtの暗号に使ったりする. NEXTAUTH_SECRET
が設定されているのであればこの設定は不要
session
この設定がわりと重要. 認証された情報をどのように持つか、という設定.
session: {
// Choose how you want to save the user session.
// The default is `"jwt"`, an encrypted JWT (JWE) stored in the session cookie.
// If you use an `adapter` however, we default it to `"database"` instead.
// You can still force a JWT session by explicitly defining `"jwt"`.
// When using `"database"`, the session cookie will only contain a `sessionToken` value,
// which is used to look up the session in the database.
strategy: "database",
// Seconds - How long until an idle session expires and is no longer valid.
maxAge: 30 * 24 * 60 * 60, // 30 days
// Seconds - Throttle how frequently to write to database to extend a session.
// Use it to limit write operations. Set to 0 to always update the database.
// Note: This option is ignored if using JSON Web Tokens
updateAge: 24 * 60 * 60, // 24 hours
}
- strategy
-
database
orjwt
.database
を設定した場合はデータベースでセッション情報を管理する.jwt
の場合はjwt
の暗号化技術に基づき情報を管理する -
database
を選択する場合はadapter
が必要となり、フロントでもデータベースを繋げられるようにしなければならない.
-
- maxAge
- 有効期限. セッション情報の有効期限
- updateAge
- セッション情報のみ有効.セッション情報の有効期限を伸ばす頻度を設定.
ここで私見.
どちらの管理方法もcookieで情報をもつことは変わりないが、そのcookieがsessionId
であるかjwt
であるかが違う.
詳細は「https://zenn.dev/tanaka_takeru/articles/3fe82159a045f7 」を読むべき. 考えることはバックエンドにどういう風に認証情報を渡すか, あるいはセキュリティリスクだと思う.
個人的に思うメリットとデメリットを記載する
↑の選択はなかなか考える必要があり一朝一夕でどちらがいいかは不明. しかし、実装難易度は「セッション」の方が簡単ではあると思う。
APIに送る時は認可したproviderのtokenをヘッダーに置く。バックエンドでは Bearer access_token
にてproviderで設定したものに対してAPI user/me みたいにgithubなどにユーザ情報が取れるかで認証
jwt
session
の strategy: "jwt"
になっている時有効
jwt: {
// The maximum age of the NextAuth.js issued JWT in seconds.
// Defaults to `session.maxAge`.
maxAge: 60 * 60 * 24 * 30,
// You can define your own encode/decode functions for signing and encryption
async encode() {},
async decode() {},
}
- maxAge
- jwtの有効期限. sessionの方にも同じキーがあるが、
strategy: "jwt"
である場合はおそらくこちらが有効期限の設定となる
- jwtの有効期限. sessionの方にも同じキーがあるが、
- その他
- encode, decodeで使うアルゴリズムを決めることができる
import { getToken } from "next-auth/jwt"
const secret = process.env.NEXTAUTH_SECRET
export default async function handler(req, res) {
// if using `NEXTAUTH_SECRET` env variable, we detect it, and you won't actually need to `secret`
// const token = await getToken({ req })
const token = await getToken({ req, secret })
console.log("JSON Web Token", token)
res.end()
}
↑のようにjwt
でheaderに載せてバックエンドに送るようなことは記載してあるが、これをどういう風にデコードすればいいかみたいなことは記載がないので、色々試してみないとこの辺は難しい
pages
pages: {
signIn: '/auth/signin',
signOut: '/auth/signout',
error: '/auth/error', // Error code passed in query string as ?error=
verifyRequest: '/auth/verify-request', // (used for check email message)
newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
}
- signIn
- REST APIであったがここで指定したページが飛ぶ
- signOut
- REST APIであったがここで指定したページが飛ぶ
- error
- エラーが起きた時に飛ぶページ
- verifyRequest
- email認証の確認ページ.EmailProviderでやる時は設定が必要なのかもしれない
- newUser
- おそらく新規userでアカウントのレコードが登録されたときに飛ばすことができるやつ
callback
それぞれの関数の振る舞いの定義
adapter
adapter
を設定している場合,oauthでログイン後そのデータを自分たちのデータベースに保存することができる仕様. 保存されるuser
情報はprofile
の内容だと思う

Providers
oauth
next-authには様々なログイン機能が用意されている.
- ユーザからのログインページへのアクセス.
- ユーザはログインしたいproviderのログインボタンを押す
- providerのログインページにリダイレクトされ、認証をする
- providerのサーバから
api/auth/callback/:provider?code=123
という感じで自身のアプリケーションにリクエストが飛ばされる - 自身のアプリケーションはその
code
をプロバイダーの方に送り返すようなリクエストを行う - 自身のアプリケーションはproviderから
access_token
を受け取り認証が完了となる
↑の流れはリンク先のシーケンス図に記載されている流れを言語化したものである
providerの各種オプションについては以下で記載していく
export default function Github<P extends GithubProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "github",
name: "GitHub",
type: "oauth",
authorization: {
url: "https://github.com/login/oauth/authorize",
params: { scope: "read:user user:email" },
},
token: "https://github.com/login/oauth/access_token",
userinfo: {
url: "https://api.github.com/user",
async request({ client, tokens }) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const profile = await client.userinfo(tokens.access_token!)
if (!profile.email) {
// If the user does not have a public email, get another via the GitHub API
// See https://docs.github.com/en/rest/users/emails#list-public-email-addresses-for-the-authenticated-user
const res = await fetch("https://api.github.com/user/emails", {
headers: { Authorization: `token ${tokens.access_token}` },
})
if (res.ok) {
const emails: GithubEmail[] = await res.json()
profile.email = (emails.find((e) => e.primary) ?? emails[0]).email
}
}
return profile
},
},
profile(profile) {
return {
id: profile.id.toString(),
name: profile.name ?? profile.login,
email: profile.email,
image: profile.avatar_url,
}
},
style: { logo: "/github.svg", bg: "#24292f", text: "#fff" },
options,
}
}
- OAuthConfig
- これを継承して独自のproviderを作成することが可能
- authorization
- signinのエンドポイント
- signin関数を押した時に飛ばされるリンク先.ここでいうとgithubのloginページに遷移する
- paramsに指定するクエリパタメータは公式ドキュメントを確認する https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#:~:text=GET https%3A//github.com/login/oauth/authorize
- token
- githubの認証トークン取得用のエンドポイント
- userinfo
- tokenのエンドポイントを叩いた後にどういうユーザで管理するかの仕上げを行う関数
- ここではユーザ情報やメール情報を取得している
- profile
- データベースの保存に用いられていたり内部のコードで使われる
userにベースとなるアクセスコントロールを付与する場合は↑のようにoauth providerにprofileを用意してやり追加する必要がある.
Credentials
email, passwordでログインを行うベーシックなやり方の認証方式.
providers: [
CredentialsProvider({
// The name to display on the sign in form (e.g. 'Sign in with...')
name: 'Credentials',
// The credentials is used to generate a suitable form on the sign in page.
// You can specify whatever fields you are expecting to be submitted.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
// You need to provide your own logic here that takes the credentials
// submitted and returns either a object representing a user or value
// that is false/null if the credentials are invalid.
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
// You can also use the `req` object to obtain additional parameters
// (i.e., the request IP address)
const res = await fetch("/your/endpoint", {
method: 'POST',
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" }
})
const user = await res.json()
// If no error and we have user data, return it
if (res.ok && user) {
return user
}
// Return null if user data could not be retrieved
return null
}
})
]
- authorize
- ログインフォームでsignin関数を実行すると
credentials
に情報がのる.独自に用意しているログイン apiを叩いて認証を行う.
- ログインフォームでsignin関数を実行すると

database
adapter
で利用できるものをnext-authは提供している. このadapter
を適用することで、oauthのログイン時にuser, account, session情報などを保存できる.
データのER図にもある通りテーブルは以下の通り
- users
- user情報. oauthであればprofileに記載してある情報が登録される
- accounts
- 認証タイプ.githubであればgithubの情報が保存されている
- sessions
- 認証session情報.ログインした時にここにレコードが存在かつ有効期限内であればそのユーザはログイン可能
- verification_tokens
- MF2の役割.将来的な話らしい

callbacks
関数を実行するタイミングで呼ばれる非同期関数を定義する
...
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
return true
},
async redirect({ url, baseUrl }) {
return baseUrl
},
async session({ session, user, token }) {
return session
},
async jwt({ token, user, account, profile, isNewUser }) {
return token
}
...
}
- singnin
- signinが終わった後に呼び出される関数
- redirect
- redirect実行される時に呼ばれ、リダイレクトしたいurlを定義する関数
- jwt
-
strategy: jwt
のみ有効. jwtの暗号化に使うpayloadを定義. - session情報に含めたい情報をreturnすると、sessionに含めることができる
-
callbacks: {
// token: デフォルトのjwt暗号化で使われる情報
// account: oauthで認証した時に使われた情報
// profile: oauthのconfig設定した情報.
async jwt({ token, account, profile }) {
// Persist the OAuth access_token and or the user id to the token right after signin
if (account) {
token.accessToken = account.access_token
token.id = profile.id
}
return token
}
}
- session
- session認証, jwt認証の両方で有効.
- デフォルトに加えsession情報をクライエントに載せたい場合は、token, userからデータを取得しreturnさせる
callbacks: {
// session: デフォルトのsession
// token: jwtでの情報
// user: session認証である時有効. データベースから取得した情報が入る
async session({ session, token, user }) {
// Send properties to the client, like an access_token and user id from a provider.
session.accessToken = token.accessToken
session.user.id = token.id
return session
}
}