[ Image Source: NASA ]
NextAuth.js は、Next.jsのサーバーレス環境向けに開発されたオープンソースの認証システムです。NginxなどのWEBサーバーが利用できない環境でも、ユーザー認証を手軽に導入できるような構成になっています。
以前に、とあるWEBアプリケーションのAdminパネルを作成していたのですが、「手軽な」「クライアントサイドの」「パスワードを使った」認証システムはないかなと考えていた矢先にNextAuthの存在を知りました。
手軽に導入できる認証システムとしてはHTTPベーシック認証が有名ですが、ベーシック認証の場合はサーバーでの設定が必要であるため、クライアントサイドだけでは導入できません。
ベーシック認証の導入も考えましたが、サーバー構成やパスワードを保存するデータベースの設定などを考えると、もっと手軽な他の選択肢はないだろうか、との思いから色々と探してみたわけです。
NextAuthは、Next.js向けの認証システムとして有名ですので、すぐにNextAuthの情報を見つけることができました。クライアントサイドだけで完結できるコンセプトがまさに僕が実装したい方向性にぴったりでしたので、まずは、公式ドキュメントを読みつつ、ざっくりと導入して様子を見ようと考えました。ところが、、、
実は、パスワードログインは、NextAuthが提供する認証システムのメインではありません。
公式ドキュメントにもはっきりと、
"NextAuth.js is designed to avoid the need to store passwords for user accounts"
(NextAuthは、ユーザーアカウントのパスワードを保存しなくてもよいように設計されています)、と記載されています。
公式ドキュメントの別の箇所でも、
"The functionality provided for credentials based authentication is intentionally limited to discourage use of passwords due to the inherent security risks associated with them"
(要するに、パスワード固有のセキュリティリスクを考慮して、推奨しない)、と記載されています。。
「パスワード固有のリスク」については明示されていませんが、確かに、パスワードに流出・盗難といったリスクがあることについては間違いありません。
公式ドキュメントのTutorialには、GithubやGoogleといった外部の認証サービスを利用するコードが紹介されていますので、NextAuthの開発者サイドがメインに想定しているのは、外部認証サービス + NextAuthというような実装なのかもしれませんね。
それでは、NextAuthを使ってパスワードログインの実装はできないのでしょうか? 一抹の不安がよぎりましたが、公式ドキュメントを読み進めるうちに、問題なく実装できることがわかりました。
実は、NextAuthはパスワードログインをあまり推奨していないとはいえ、実装方法についてわりと詳しく記載されています。ただし、必要な情報が公式ドキュメントの様々な場所に分散しているので、必要なコードを揃えるのに少してこずりました。
試行錯誤の末、NextAuthを用いたパスワードログインのテンプレートを作りましたので、実装する際に遭遇したエラーを振り返りながら注意するべきポイントなどを説明していきたいと思います。
この記事で想定している実装は、次のようなケースになります。
Github認証など外部サービスの利用でも良い場合は、公式ドキュメントに実装例がありますので、そちらをご覧ください。
これからご説明する実装方法に基づいて、次のようなNext.jsのサンプルアプリケーションを作成しました。実際の動作を確認することで、これから説明するNextAuthを使ったログインの実装がよりイメージしやすくなると思います (ユーザー名: mike、パスワード: helloworld)。
(プロジェクトのリンクが開きます)
まずはNextAuthをインストールをインストールします。npmのページもご覧ください。
npm i next-auth
NextAuthを使うと、WEBサーバーなしでクライアントサイドだけで認証を行うことができます。そのために必要となるのがサーバーレスファンクションです。
サーバーレスファンクションとは、HTTPリクエストの受領とリスポンスの応答を行うためのコードです。Node.jsなどのバンクエンド言語で書かれています。
Next.jsの場合、pagesフォルダ内にデフォルトでapiフォルダが設置されており、全てのサーバーレスファンクションのファイルをこの中に格納して利用します。
NextAuthの場合、コアとなるファンクションのファイルは[...nextauth].js です (設置場所は、pages/api/auth/[...nextauth].ts)。まずは、サンプルコードを見てみましょう。
[...nextauth].js
(公式ドキュメント)
import CredentialsProvider from "next-auth/providers/credentials";
...
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
const user = { id: 1, name: "J Smith", email: "jsmith@example.com" }
if (user) {
return user
} else {
return null
}
}
})
]
基本的にはサンプルコード通りの実装で良いのですが、このままでは、エラーが出てしまいます。
結論から言うと、NextAuth version 4 (2021年6月リリース) では、サーバーレスファンクションにSecretとサイトURLの二つ値を渡してあげないと、productionでエラーが出る仕様になっています(2022/02現在)。
では具体的にどのように設定するかというと、この二つの値を環境変数として設定するだけでOKです。.envファイルを作成して、それぞれNEXTAUTH_SECRET、NEXTAUTH_URLとして設定します。
この設定で実装して問題なく動くことを確認済みですが、NextAuth version 4のアップグレードガイドには、サーバーレスファンクションのOptionオブジェクトのプロパティの一つとして、secretの値を明示的に設定する方法が記載されています (以下のProductionコードではこの点も反映させています)。
上記の点を考慮すると次のようなコードになりました(Typescriptで書いています)。
[...nextauth].ts
(Productionバージョン)
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
const isCorrectCredentials = (credentials: Record<"username" | "password", string> | undefined) =>
credentials && credentials.username === process.env.NEXTAUTH_USERNAME &&
credentials && credentials.password === process.env.NEXTAUTH_PASSWORD;
const options = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
if (isCorrectCredentials(credentials)) {
const user = { id: 1, name: "admin" }
return user
} else {
return null
}
}
}),
],
secret: process.env.NEXTAUTH_SECRET //オプションとしてsecretプロパティを設定
};
const NextAuthFn = (req: NextApiRequest, res: NextApiResponse) => NextAuth(req, res, options);
export default NextAuthFn
NEXTAUTH_URL=https://example.com //**重要**
NEXTAUTH_USERNAME=mike
NEXTAUTH_PASSWORD=helloworld
NEXTAUTH_SECRET=1234567 //**重要**
それでは、次にサーバーレスファンクションをアプリケーション本体と接続する方法について見ていきましょう。ここでのポイントは、最新版のversion 4で導入されたSessionProviderです。
version 4の仕様では、SessionProviderの設定がMUSTになりました。
とはいえ、実装方法はシンプルです。_app.tsxファイルの中で、
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { SessionProvider} from "next-auth/react";
function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
</>
)
}
export default MyApp
sx
それでは、ユーザーがサイトにアクセスした時に最初に表示されるページ(index.tsx)について見ていきましょう。
indexページには、ログインページにリダイレクトする"Signin"ボタンが設置されています。
Signinボタンをクリックすると、認証画面が表示されます。また、入力したユーザー名とパスワードが正しければ、index.tsxページに再度リダイレクトされて、認証が成功した場合の画面が表示さます。
dataは3つの値を取ります(Session / undefined / null)。Sessionはログイン成功を示します。
また、statusは、の3つの状態を示しています (loading / authenticate / unauthenticated)。
この二つの変数の利用方法は様々だと思いますが、以下に示す実装例では、data stateをsessionという変数に置き換えて、session(Sessionが存在する=ログイン成功)ならログイン後のページを表示、!session(Sessionが存在しない=ログイン失敗)ならSigninボタンを表示するというように、sessionの値次第で表示する画面を切り替える方法を採用しています。
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { useSession, signIn, signOut } from "next-auth/react"
const Home: NextPage = () => {
const { data: session, status } = useSession()
const loading = status === "loading"
if (loading) {
return Loading...
;
}
const handleSignIn = () => {
signIn()
}
const handleSignOut = () => {
signOut()
}
return (
<div className={styles.container}>
<div className="mt-5">
{!session && (
<>
Not signed in <br />
<button className ="mt-3 bg-blue-500 hover:bg-blue-400 text-white w-20 p-2 rounded focus:outline-none cursor-pointer text-sm xs:text-base;" onClick={handleSignIn}>Sign in</button>
</>
)}
{session && (
<>
Signed in as {session.user && session.user.name} <br />
<button className ="mt-3 bg-green-500 hover:bg-green-400 text-white w-20 p-2 rounded focus:outline-none cursor-pointer text-sm xs:text-base;" onClick={handleSignOut}>Sign out</button>
</>
)}
</div>
{session && <>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
</main>
</>}
</div>
)
}
export default Home
以上、NextAuth.jsを利用したパスワードログインの実装方法をご紹介しました。
NextAuth.jsを使うとNext.jsのサーバーレス認証が手軽に導入できるので、パスワード管理にしっかりと注意すれば、 色々な場面に適用できるのではないでしょうか✨