Closed9

next auth 調査

hanpenmanhanpenman

next authは認証を簡単にしてくれるもの. Google Provider, Github Provider, email認証が存在する.

https://authjs.dev/

  • authという名前になっているのだが、便宜上next-authとして記載していく

https://next-auth.js.org/getting-started/introduction

認証のデータについて

  • 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の最新情報の技術に則る形で実装を試みている

hanpenmanhanpenman

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となる

セッションを使うとき

https://next-auth.js.org/getting-started/client

データ

{
  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がないので↑のように使うことができる.

hanpenmanhanpenman

REST API

https://next-auth.js.org/getting-started/rest-api

  • GET /api/auth/signin
    • ログインページに飛ぶ
    • next optionのpagesにて場所は指定可能
  • POST /api/auth/signin/:provider
    • 指定したproviderでのログイン. この時getリクエストで /api/auth/csrfからcsrf tokenを取得しheaderに付与してPOSTしている
  • 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呼んでいるのだと思う
  • GET /api/auth/csrf
    • ログイン, ログアウトで必要
  • GET /api/auth/providers
    • 設定しているproviderの情報. (バレてはいけない情報が書いているわけではないよ)
hanpenmanhanpenman

https://next-auth.js.org/getting-started/typescript

sessionjwtの型の拡張方法について記載

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を付与したりする

hanpenmanhanpenman

next-auth 初期設定

basic

https://next-auth.js.org/configuration/initialization

// /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のオプションの中にreqresが使えるような書き方が特徴的である. callbackjwtなどで細工ができる

hanpenmanhanpenman

next-auth options

https://next-auth.js.org/configuration/options

env

  • NEXTAUTH_URL
  • NEXTAUTH_SECRET
    • jwtの暗号化に使われる
  • NEXTAUTH_URL_INTERNAL
    • NEXTAUTH_URLが正規のurlではない場合に使うとのこと. vercel用なのかな.

options

providers

認証する方式. githubgoogleemailで認証するかを決定する

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 or jwt. 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

sessionstrategy: "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"である場合はおそらくこちらが有効期限の設定となる
  • その他
    • 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の内容だと思う

hanpenmanhanpenman

Providers

https://next-auth.js.org/configuration/providers/oauth

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
  • token
    • githubの認証トークン取得用のエンドポイント
  • userinfo
  • tokenのエンドポイントを叩いた後にどういうユーザで管理するかの仕上げを行う関数
  • ここではユーザ情報やメール情報を取得している
  • profile
    • データベースの保存に用いられていたり内部のコードで使われる

https://authjs.dev/guides/basics/role-based-access-control

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を叩いて認証を行う.
hanpenmanhanpenman

database

https://authjs.dev/getting-started/adapters

adapterで利用できるものをnext-authは提供している. このadapterを適用することで、oauthのログイン時にuser, account, session情報などを保存できる.

データのER図にもある通りテーブルは以下の通り

  • users
    • user情報. oauthであればprofileに記載してある情報が登録される
  • accounts
    • 認証タイプ.githubであればgithubの情報が保存されている
  • sessions
    • 認証session情報.ログインした時にここにレコードが存在かつ有効期限内であればそのユーザはログイン可能
  • verification_tokens
    • MF2の役割.将来的な話らしい
hanpenmanhanpenman

callbacks

https://next-auth.js.org/configuration/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
  }
}
このスクラップは2024/01/04にクローズされました