NextAuth v5で外部APIを使った認証を実装する
はじめに
この記事は、最近Next.jsを使い始めた初心者が、NextAuthと外部(?)APIを使って簡易的な認証を実装するまでを記した記事です。
- 情報を渡すとAPIからトークンが発行され、そのトークンを元にAPIにリクエストを送る
- ↑のトークンをNextAuthのセッション情報に保存する
- ログインしていないと見れないページを実装し、そのページでAPIのリクエストの結果を表示する
上のような状況をどのように実装するか、そしてNextAuthの基本的な使い方と、どのようにAPIにリクエストを送るかを解説しています。
環境
- Next.js 14.1.1 (App Router)
- NextAuth V5 Beta
- Bun 1.1.29
APIの仕様
この記事では、localhost:3000
にAPIサーバーが立っていると仮定します。
仕様はだいたい以下の通りです。
-
/login
にPOST
リクエスト(内容にusername
を含める)を送ると、token
が含まれたレスポンスが返ってくる -
Session
ヘッダにトークンを入れた上で、/use
にGET
リクエストを送ると、ユーザー名(username
)とメッセージ(message
)が含まれたレスポンスが返ってくる
NextAuthをセットアップ
ということで、Next.jsとNextAuthをセットアップしていきます。
Next.jsをセットアップ
まずはNextAuthのインストール...と行きたいところですが、その前にNext.jsの雛形を作ります。
コマンドは以下の通りです。
bun create next-app@14
ここでバージョンを指定しているのは、現在(2024/10/1時点)バージョンを指定しないとNext.js 15(RC)が使用されるためです。
15でも動くかどうかに不安があったため、一応Next.js 14を使っています。
不要なファイルを削除する
雛形が作成できたら、不要なファイルを削除します。
ここではフォントやCSSを削除し、最低限必要なファイルのみにしました。
ポートを設定
Next.jsはデフォルトだとlocalhost:3000
で開発サーバーを起動するのですが、今回このポートはすでにAPIが使っています。
この記事では代わりに5500
番を使います。
ポートは--port
オプションで指定することができます。
package.json
を編集("dev": "next dev --port 5500"
など)しておくと、いちいちポートを指定しなくてもいいので楽です。
設定が済んだら、以下のコマンドで開発サーバーを起動できます。
bun run dev
起動したら http://localhost:5500 をブラウザで開いてみてください。
ページを作成
では次に、Next.jsで以下のページを作成します。
ページはどのようなものでも大丈夫です。
この記事では以下のようにしてみました。
-
/
: いろいろなページへのリンクを載せるページ -
/about
: ログインしなくても見れるページ -
/protected
: ログインが必要なページ
/protected
には以下の情報を載せようと思います。
- ログイン中のユーザーの名前
- アクセストークンを使ってAPIから取得したデータ
しかし、現状はまだログイン機能がないので実装することができません。
ここはひとまずほぼ空のページを置いておきます。
NextAuthをインストール
ということで、いよいよNextAuthをインストールします。
しかし、ここで一つ注意が必要です。
どうやら、NextAuth v4はNext.js14に対応していないらしいです。
この記事ではNext.js 14を使用しているため、現在Beta版であるNextAuth v5を使用する必要があります。
インストールには以下のコマンドを使用します。
bun add next-auth@beta
インストールの詳細は公式ドキュメントをご覧ください。
まずは作動させる
準備は整ったので、まずはNextAuthを作動させてみようと思います。
ログインが必要なページにアクセスしたら/login
にリダイレクトするよう設定していきます。
auth.ts
を作成
/src/auth.ts
はNextAuthの設定を書くファイルです。
また、NextAuth
という関数を呼び出して、ログインやセッション情報の取得に必要な関数をエクスポートする役割も担っています。
本格的な設定は後回しにして、今はとりあえず以下のようにしておきます。
import NextAuth from "next-auth";
export const { auth } = NextAuth({
providers: [],
callbacks: {
authorized() {
return false
},
}
})
authorized()
とは?
上の設定ファイルでは、callbacks.authrized
を定義しました。
ここで設定した関数は、ユーザーが認可されているかを判断するのに使われます。
イメージ的には、ユーザーが何かのページにアクセスした時に呼ばれ、そのページにアクセスする権限があるかどうかをboolean
で返すという感じです。
また、Response.redirect
を返すことで、リダイレクトするようにもできます。
今回の設定は以下のようになっています。
authorized() {
return false
}
これはどのような場合でも全てのページでアクセスを許可しないというものです。
ひとまずこのようになっていますが、これだと以下のような問題があります。
- ログインが不要なページでもアクセスできない
- ログインしていてもページにアクセスできない
そのため、この設定は後で変更する必要があります。
詳細は公式ドキュメントをご覧ください。
middleware.ts
を作成
middleware.ts
は、Next.jsにおけるミドルウェアを定義するファイルです。
ミドルウェアはNexhAuthではなくNext.jsの機能であり、NexhAuthはそれに乗っかる形となっています。
このファイルでは、ミドルウェアとなる関数をmiddleware
としてエクスポートします。
また、config
オブジェクトでミドルウェアが適用される範囲を指定します。
export { auth as middleware } from '@/auth'
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)']
}
ミドルウェアの詳細はNext.jsドキュメントをご覧ください。
また、matcher
はこちらを使用しています。
これは静的ファイルなどを除くほぼ全てのルートにマッチさせるものです。
さて、これでページの保護ができたはずです。
開発サーバーを立て、/
や/about
などの好きなページにアクセスしてみます。
すると、/api/auth/signin
にリダイレクトされ、404が表示されるはずです。
このように、アクセス許可がない(=auth.ts
のcallback.authorized
でfalse
を返した)ページはログインページにリダイレクトされるようになります。
シークレットを作成
無事に動いて嬉しいな、と思いながら徐にサーバー側のコンソールを開くと、何やらエラーが表示されています。
[auth][error] MissingSecret: Please define a `secret`. Read more at https://errors.authjs.dev#missingsecret
どうやらシークレットとやらを定義しないといけないようです。
これはAUTH_SECRET
という環境変数がないときに表示されるものです。
AUTH_SECRET
はNextAuthがトークンなどを暗号化する時に使われるもので、コマンドで作成することができます。
ということで、以下のコマンドを入力します。
bunx auth secret
実行すると/.env.local
ファイルが作成され、中にはAUTH_SECRET
が書かれていると思います。
AUTH_SECRET
ができたので、開発サーバーを再起動します。
ブラウザのクッキーを削除し、再度ページにアクセスすると、サーバー側からエラーが消えるはずです。
ログインページのURLを変更
現在認証が必要なページにアクセスすると、/api/auth/signin
にリダイレクトされてしまいます。
この記事では/login
にリダイレクトしたいので、設定を変更します。
環境変数を追加
まずは.env.local
にAUTH_URL
という環境変数を追加します。
これがないと、私の環境ではビルドした時にエラーが発生してしまいました。
以下のようにファイルを編集します。
AUTH_SECRET="略" # Added by `npx auth`. Read more: https://cli.authjs.dev
AUTH_URL="http://localhost:5500/login" # ポートは実際に使っているものを使用
設定ファイルに追記
次に、auth.ts
の設定を変更します。
ログインページのURLはpages.signIn
で設定できます。
providers: [],
+ pages: {
+ signIn: '/login'
+ },
callbacks: {
authorized() {
以上の設定をしたら、もう一度開発サーバーを起動し直し、好きなページにアクセスしてみてください。
/login
にリダイレクトされ、画面に404が表示されたら成功です。
ログインフォームを作成
ページにアクセスしたら/login
にリダイレクトされるまではできました。
しかし現状だと、/login
にアクセスしても404が表示されてしまいます。
ということで、次はログインフォームを作成し、セッションにユーザーの情報が保存されるようにします。
この記事では、ログインフォームを以下のように実装します。
-
/login
ページは自力で作成 - Server Actionsを使用してログイン
- APIにリクエストを送るのはまだ先で、ひとまず動くようにする
/login
ページを作成
まずはログイン用のページを作成します。
今回はユーザー名さえあればログインできるという仕様なので、フォームにはユーザー名を入力するボックスを設置します。
ということで、/src/app/login/page.tsx
を作成し、以下のように編集します。
export default function Page() {
return (
<main>
<h1>ログインフォーム</h1>
<form>
<div>
<label htmlFor="username">ユーザー名</label>
<input type="text" name="username" id="username"/>
</div>
<button type="submit">送信</button>
</form>
</main>
)
}
これで/login
にアクセスしても404が表示されなくなり、代わりにログインフォームが表示されるようになりました。
Server Actionsを実装
ページの外見はできましたが、現在このフォームで送信ボタンを押しても特に何も起きません。
ということで、フォームが送信された時の動作をServer Actionsで実装します。
login/page.tsx
を以下のように編集します。
+ import { signIn } from "@/auth"
+ async function login(formData: FormData) {
+ 'use server'
+ // Credentialプロバイダを使ってログイン
+ await signIn('credentials', formData)
+ }
export default function Page() {
<main>
<h1>ログインフォーム</h1>
- <form>
+ <form action={login}>
また、@/auth
からsignIn
という関数をインポートしているのですが、現状このファイルではそんな関数はエクスポートしていないので、型エラーが出ています。
ということでエクスポートします。
- export const { auth } = NextAuth({
+ export const { auth, signIn } = NextAuth({
providers: [],
これでフォームが送信されたらsignIn
関数が呼び出されるようになりました。
保護されたルートを編集
現状の設定だと、/
や/about
などのログインしていなくても見れるページが見れないという問題があります。
このままだと非常にやりづらいので、ここでこの設定を変更したいと思います。
変更する設定はauth.ts
のcallbacks.autorized
です。
この設定についてはauthorized()
とは?に書いているので、そちらも併せてご覧ください。
ということで、auth.ts
を以下のように編集します。
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth
const isOnProtected = nextUrl.pathname.startsWith('/protected')
const isOnLogin = nextUrl.pathname.startsWith('/login')
// ログイン済みでログインページにアクセスしている場合
if (isLoggedIn && isOnLogin) {
const callBackUrl = nextUrl.searchParams.get('callbackUrl')
if (callBackUrl) {
// コールバックURLが指定されていたらリダイレクト
return Response.redirect(callBackUrl)
} else {
return true
}
}
// ログインしていなくても見れるページの場合
if (!isOnProtected) return true
// ログイン済みの場合
if (isLoggedIn) return true
return false
},
}
編集できたら、/
や/about
などのログインが不要なページにアクセスしてみてください。
/login
にリダイレクトされなくなり、ページの内容が表示されれば成功です。
ログインのロジックを実装
では次に、ログイン方法を追加していきます。
現状だとログイン方法が登録されていないため、ログインフォームを使ってもログインできません。
ログイン方法は、auth.ts
のproviders
に追加します。
providers
とはNextAuthのプロバイダを追加する場所です。
プロバイダにはGitHubやGoogleなど様々な種類があり、実装したいログイン方法に対応したプロバイダを使うと、簡単に認証を実装することができます。
今回はCredentials
というプロバイダを使います。
このプロバイダは自分でログインの手順を1から定義する時に使うものです。
詳しくは公式ドキュメントをご覧ください。
プロバイダを追加するには、auth.ts
を以下のように編集します。
import Credentials from "next-auth/providers/credentials";
export const { auth, signIn } = NextAuth({
providers: [Credentials({
authorize(credentials) {
const username = credentials.username
if (typeof username !== 'string') return null
// TODO 認証処理を書く
return { username }
},
})],
// 略
このコードでは、プロバイダにCredentials
を追加し、authorize
の設定を行っています。
authorize
は、signIn
が呼び出された時など、ログインの処理を定義する関数です。
この関数は以下の値を返す必要があります。
- 認証に成功した時: ユーザー情報(
User
インターフェースの値) - 認証に失敗した時:
null
また、このコードだとUser
にusername
というプロパティがないため、型エラーが出てしまいます。
この問題を解決するため、以下のようなモジュール宣言を追加します。
declare module 'next-auth' {
interface User {
username: string
}
}
これで型エラーが出なくなり、User
にusername
プロパティがあることが明治できたと思います。
このコードについての詳細は公式ドキュメントをご覧ください。
ログイン方法が追加できたので、実際にログインしてみようと思います。
以下のような手順が踏めれば成功です。
-
/protected
(認証が必要なページ)にアクセスする -
/login
にリダイレクトされる - フォームにユーザー名を入力し、ログインボタンを押す
- ログインされ、
/protected
にリダイレクトされる
ユーザー名を表示
ログインができたということで、次は画面にユーザー名を表示してみたいと思います。
そのためには、当然コード上でユーザー名を取得する必要があります。
セッションの情報は/src/auth.ts
からエクスポートされているauth
関数で取得できるみたいです。
この関数はサーバーコンポーネント上で使えるみたいです。
ということで、さっそく試してみましょう。
protected/page.tsx
を以下のように編集してみます。
+ import { auth } from "@/auth";
export default async function Page() {
+ const session = await auth()
+ const username = session?.user?.username
return (
<main>
<h1>ログインが必要なページ</h1>
+ <p>ユーザー名: {username}</p>
</main>
)
}
これでブラウザを確認してみると...あれ、ユーザー名が表示されていません。
これはいったい何故でしょうか。
データを受け渡す
デフォルトで用意されているユーザーの情報以外を使う場合、手動でデータの受け渡しを定義する必要があります。
データは大まかに以下のように流れていきます。
-
Credential
のauthorize
で返したUser
の値 -
callback.jwt
で返したJWT
トークンの値 -
callback.session
で返した`Sessionの値 -
auth
で取得できるセッションの情報
詳細は公式ドキュメントをご覧ください。
ということで、コードを以下のように編集します。
// ...
callbacks: {
// ...
jwt({ token, user }) {
// userが空でないならtoken.userを更新
if(user?.username) token.user = user
return token
},
session({ session, token }) {
return {
...session,
user: token.user
}
},
}
}
また、この変更によりトークンの型が変わったため、型定義を修正する必要があります。
以下のようにコードを追記してください。
import type { JWT } from 'next-auth/jwt'
declare module 'next-auth/jwt' {
interface JWT {
user: DefaultSession['user']
}
}
編集後、ログインした状態で/protected
にアクセスすると、ユーザー名が表示されているはずです。
APIにリクエスト
ログインの実装ができたということで、次はAPIを使用してトークンを取得し、そのトークンを使ってデータを取得しようと思います。
ログイン時にAPIからトークンを取得
まずはAPIにリクエストを送るためのトークンを取得します。
この記事では、http://localhost:3000
にリクエストすると、token
プロパティからセッショントークンが返ってくる想定です。
取得は簡単で、プロバイダにリクエストのコードを書くだけです。
providers: [Credentials({
async authorize(credentials) {
const username = credentials.username
if (typeof username !== 'string') return null
+ const res = await fetch('http://localhost:3000/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ username })
+ })
+ if (!res.ok) return null
+
+ const { token: sessionToken } = await res.json()
+ return { username, sessionToken }
}
ここでは、設定が面倒なのでユーザーの情報にセッショントークンを入れています。
ユーザー情報にトークンを入れておくと、今回の場合はjwt
やsession
に変更を加えなくてもトークンが取得できるようになります。
また、ユーザー情報にsessionToken
というプロパティを追加したので、一緒に型定義も修正します。
declare module 'next-auth' {
interface User {
username: string
+ sessionToken: string
}
}
これでログイン状態の時にトークンを取得できるようになりました。
セッショントークンを取得する
トークンはユーザー名と同じように取得することができます。
例えばこのような感じです。
const session = await auth()
const { username, sessionToken } = session?.user ?? {}
トークンを使ってAPIにリクエスト
ここまで来たらあとは簡単です。
取得したトークンをリクエストヘッダに含め、返ってきたレスポンスを画面に表示してみようと思います。
この記事ではhttp://localhost:3000/use
にSession
ヘッダを含めてGET
リクエストを送ると、message
と自分のユーザー名が返ってくるAPIを想定しています。
ということで、/protected
にレスポンスの内容を表示してみます。
実装は例えばこのようになります。
export default async function Page() {
const session = await auth()
const { username, sessionToken } = session?.user ?? {}
const { data } = await fetch('http://localhost:3000/use', {
headers: new Headers({
"Session": sessionToken ?? '' // 型エラー防止
})
}).then(res => res.json())
return (
<main>
<h1>ログインが必要なページ</h1>
{/* ... */}
<p>
取得データ: {data?.message} <br />
取得してきたユーザー名: {data?.username} <br />
</p>
</main>
)
}
編集後、ログインした状態で/protected
にアクセスすると、APIから取得したデータが表示されると思います。
おまけ:ログアウトの実装
簡単なログアウトボタンをServer Actionsを使って実装した例がこちらです。
import { signOut } from "@/auth";
function Logout() {
return (
<form action={async () => {
'use server'
await signOut({ redirectTo: '/' })
}}>
<button>ログアウト</button>
</form>
)
}
また、このコンポーネントを使用するには/src/auth.ts
に以下の変更を加える必要があります。
import Credentials from "next-auth/providers/credentials";
- export const { auth, signIn } = NextAuth({
+ export const { auth, signIn, signOut } = NextAuth({
以上です。ここまで読んでいただきありがとうございました。
参考
Discussion