Next.jsでClerkを使った認証(Authentication)の基礎
認証(Authentication) はアプリケーションを構築する上で重要な機能の一つでどのライブラリ/製品を利用して、どのように設定を行えばいいのか悩んでいる人も多いのではないでしょうか。Next.js の Authentication といえば Auth.js(旧 NextAuth.js)が有名ですが本文書で最近耳にする機会も増え、ユーザ認証とユーザ管理を簡単に行うことができるClerkの動作確認を行います。オープンソースの製品ではありませんが設定もシンプルで無料枠もあるので支払い情報の入力なしで利用することができます。利用できるフレームワークは React ベースのフレームワーク(React, Next.js, Remix, RedwoodJS, Gatsby, Expo)と JavaScript です。本文書では Next.js を利用して Clerk の基本的な機能の動作確認を行っています。
目次
Clerk アカウントの作成
Cleak のアカウントの作成を行うためCleark のサイトにアクセスします。
画面右上の”Sign up”のリンクをクリックします。アカウントの作成画面が表示されるので Github, Google またはメールアドレスを利用してアカウントを作成することができます。
本文書では Google アカウントを利用してアカウントの作成を行うので”Continue with Google”ボタンをクリックするとアカウントの選択画面が表示されるので利用する Google アカウントを選択してください。
アプリケーションの追加画面が表示されるので”Add application”をクリックしてください。
Application name には任意の名前のアプリケーション名を入力してユーザのサインアップの方法を選択してください。ここではデフォルトに設定されている”Email address”と Google を選択して”CREATE APPLICATION”ボタンをクリックします。
プロジェクトの作成が完了してダッシュボード画面が表示されます。利用するフレームワークによってインストールするライブラリ/設定が異なるのでその手順がフレームワーク毎に表示されます。これから作成するプロジェクトで利用する API Keys の情報が表示されます。このキーを利用して設定を行います。
Next.js プロジェクトの作成
動作確認を行う Next.js プロジェクトの作成を行います。プロジェクト名は任意の名前をつけることができますがここでは next-js-clerk という名前にしています。TypeScript, ESLint など利用するかどうか選択することができますが残りの選択肢はすべてデフォルトを選択しています。srcはデフォルトの”No”を選択しています。
% npx create-next-app@latest
Need to install the following packages:
create-next-app@14.2.4
Ok to proceed? (y) y
✔ What is your project named? … next-js-clerk
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Using npm.
Initializing project with template: app-tw
Installing dependencies:
- react
- react-dom
- next
Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
- eslint
- eslint-config-next
//略
Clerk の初期設定
プロジェクトの作成が完了したらプロジェクトディレクトリに移動して Clerk’s Next.js SDK のインストールを行います。
% cd next-js-clerk
% npm install @clerk/nextjs
インストール後の package.json ファイルを確認して動作確認したパッケージのバージョンを確認しておきます。
{
"name": "next-js-clerk",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@clerk/nextjs": "^5.2.2",
"next": "14.2.4",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.4",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
環境変数を保存する.env.localファイルをプロジェクトディレクトリ直下に作成して Clerk の管理画面に表示されている API Keys のコピー&ペーストします。
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_YWJvdmUtY2xhbS05Ny5jbGVyay5aY2NvdW50cy5kZXYk
CLERK_SECRET_KEY=sk_test_yE7p3tglzokoPEYU86B1NrN031za3w544yE0VwJzUQ
上記の値はそのままコピーをしても利用することができないので各自がClerkのアカウントを作成して取得した API Keys を設定してください。
Clerkが管理する認証情報をアプリケーション上で利用できるように app ディレクトリの layout.tsx ファイルに<ClerkProvider>の設定を行う必要があります。
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { ClerkProvider } from '@clerk/nextjs';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ClerkProvider>
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
</ClerkProvider>
);
}
Protect Route
どの URL にアクセスした場合に認証を行うかを決めるために Next.js の Middleware の機能を利用します。プロジェクトディレクトリ直下にmiddleware.ts ファイルを作成して以下のコードを記述します。下記の設定では/api を含めすべてのルート(URL)に対して設定を行っています。
import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
設定が完了したら”npm run dev”コマンドを実行して開発サーバを起動します。
% npm run dev
> next-js-clerk@0.1.0 dev
> next dev
▲ Next.js 14.2.4
- Local: http://localhost:3000
- Environments: .env.local
✓ Starting...
✓ Ready in 5.9s
○ Compiling /middleware ...
✓ Compiled /middleware in 509ms (179 modules)
○ Compiling / ...
✓ Compiled / in 2.9s (818 modules)
GET / 200 in 3393ms
✓ Compiled in 432ms (323 modules)
○ Compiling /favicon.ico ...
✓ Compiled /favicon.ico in 766ms (515 modules)
ブラウザから http://localhost:3000 にアクセスを行います。トップページが表示されます。
デフォルトで設定したmiddlewareではどのルーとに対してもアクセスの制限を行っていないためどのページにもアクセスすることができます。
localhost:3000にアクセスする場合でもユーザ認証が完了していなければページが表示されないようにmiddleware.tsファイルを更新します。createRouteMatcher関数で引数に配列で”/”を設定しています。
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher(['/']);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect();
});
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
再度localhost:3000にアクセスするとリダイレクトが行われ、下記のサインイン画面が表示されます。URLを見ると外部のサーバ上のページが開いていることがわかります。
サインイン、サインアウトページの設定
先ほどアプリケーションにアクセスすると外部のサイトにリダイレクトされサインイン画面が表示されることを確認しました。Clerk では事前作成されたサインインコンポーネントが準備されているのでそのコンポーネントを利用することでローカル上でサインイン画面が表示されます。
app ディレクトリの下に sign-in/[[…sign-in]]ディレクトリを作成しその下に page.tsx を作成して以下のコードを記述します。
import { SignIn } from "@clerk/nextjs";
export default function Page() {
return <SignIn />;
}
app ディレクトリの下に sign-up/[[…sign-up]]ディレクトリを作成しその下に page.tsx を作成して以下のコードを記述します。
import { SignUp } from "@clerk/nextjs";
export default function Page() {
return <SignUp />;
}
.env.localファイルに以下の環境変数を追加します。
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
設定後に http://localhost:3000 にアクセスすると外部のサイトにリダイレクトされずローカルサイトのサインイン画面が表示されます。
ユーザのサインイン
Google アカウントまたはメールアドレスを利用してサインインを行ってみましょう。ここでは Google アカウントを利用するため表示されたサインイン画面で”Continue with Google”ボタンをクリックします。
サインインが完了するとトップページが表示されます。
サインインが完了したら Clerk のダッシュボードで Users ページにアクセスします。Dashboardにもユーザが登録されたことを祝うメッセージが表示されます。Total users, Active usersの値が1になっていることがわかります。
サイドメニューから”Users”をクリックするとサインインしたユーザが登録されていることが確認できます。サインインしたユーザは Clerk のダッシュボードで管理することができます。
ユーザのサインアウト
サインインを行いましたが現在の設定ではサインアウトを行う方法がありません。サインアウトを行うためのボタンもコンポーネントとして準備されているので app/page.tsx ファイルで SignOutButton コンポーネントを import します。
import { SignOutButton } from '@clerk/nextjs';
export default function Home() {
return (
<div>
<SignOutButton />
</div>
);
}
スタイルが何も設定されていない Sign Out の文字列が表示されます。button 要素なのでクリックすることができます。
クリックするとサインアウトすることができます。サインアウトするとすぐにサインイン画面が表示されます。
表示されるボタンは SignOutButton コンポーネントの内側に button 要素を挿入しスタイルを適用することでカスタマイズすることができます。
import { SignOutButton } from '@clerk/nextjs';
export default function Home() {
return (
<div className="m-4">
<SignOutButton>
<button className="px-4 py-2 rounded-lg bg-blue-500 text-white text-sm">
サインアウト
</button>
</SignOutButton>
</div>
);
}
UserButton の設定
SignOutButton コンポーネントを利用してサインアウトを行うことができましたが UserButton コンポーネントを利用してサインアウトを行うことができます。UserButton コンポーネントはサインアウトだけではなくアカウントの管理も行うことができます。
import { UserButton } from '@clerk/nextjs';
export default function Home() {
return (
<div>
<UserButton afterSignOutUrl="/" />
</div>
);
}
UserButton コンポーネントを追加するとボタンが表示されるのでボタンをクリックするとドロップダウンメニューで”Manage account”と”Sign Out”のメニューが表示されます。
“Sign Out”をクリックするとサインアウトが行われサインイン画面が表示されます。
Manage account をクリックするとアカウント名や Email アドレス、接続してきたデバイスの情報などが表示されます。
複数ページ環境での動作確認
サインイン、サインアウトの方法が理解できたので Dashboard ページを追加して複数ページ環境で Clerk の動作確認を行なっていきます。
Dashboard ページの追加
サインイン後に表示されるダッシュボードページの作成を行います。app ディレクトリに dashboard ディレクトリを作成して page.tsx ファイルを作成します。
export default function DashboardPage() {
return <h1>Dashboard</h1>;
}
Layout の設定
各ページに共有のヘッダーを設定できるようにプロジェクトディレクトリ直下に components コンポーネントを作成して NavBar.tsx ファイルを作成します。
export const NavBar = () => {
return (
<nav className="flex justify-between items-center h-12 mx-4">
<div className="font-bold">Clerk Auth</div>
</nav>
);
};
作成した NavBar コンポーネントを layout.tsx ファイルで import します。
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { ClerkProvider } from '@clerk/nextjs';
import { NavBar } from '@/components/NavBar';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en">
<body className={inter.className}>
<NavBar />
<main className="mx-4">{children}</main>
</body>
</html>
</ClerkProvider>
);
}
app/page.tsx ファイルの更新も行います。
export default function Home() {
return <h1>Home</h1>;
}
middleware.ts ファイルではサインインが行われていなくても”/“にアクセスできるように createRouteMatcher関数の引数を更新します。/dashboardにアクセスを行うにはサインインが必要です。
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect();
});
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
デフォルトで設定されている global.css ファイルのスタイルを削除し、Tailwind CSS の設定のみ残します。
@tailwind base;
@tailwind components;
@tailwind utilities;
ここまでの設定で http://localhost:3000 にアクセスすると Home 画面が表示されます。
サインインボタンの設定
”/“ページにサインインボタンを追加してサインインが完了したら Dashboard ページにリダイレクトされるように SignInButton コンポーネントを利用します。forceRedirectUrl props を利用することでサインインが成功した場合にリダイレクトされる場所を指定しています。mode で modal を設定することでモーダルウィンドウとしてサインイン画面が表示されます。
import { SignInButton } from '@clerk/nextjs';
export const NavBar = () => {
return (
<nav className="flex justify-between items-center h-12 mx-4">
<div className="font-bold">Clerk Auth</div>
<SignInButton forceRedirectUrl="/dashboard" mode="modal" />
</nav>
);
};
文字列で”Sign in”ボタンが表示されます。
“Sign in”ボタンをクリックして表示されたサインイン画面からサインインを行うと/dashboard にリダイレクトされます。リダイレクトされると Dashboard 画面が表示されます。
SignedOut コンポーネントによる非表示設定
サインインが完了しているにも関わらず”Sign in”ボタンが表示されている状態なのでサインインが行われていない場合のみ”Sign in”ボタンが表示されるように SignedOut コンポーネントを利用します。SignInButton コンポーネントを SignedOut コンポーネントでラップします。
import { SignInButton, SignedOut } from '@clerk/nextjs';
export const NavBar = () => {
return (
<nav className="flex justify-between items-center h-12 mx-4">
<div className="font-bold">Clerk Auth</div>
<SignedOut>
<SignInButton redirectUrl="/dashboard" mode="modal" />
</SignedOut>
</nav>
);
};
“Sign in”ボタンが非表示となります。サインインが行われていない場合のみ”Sign in”ボタンが表示されるようになりました。
SignedIn コンポーネントによる表示設定
サインインが行われている場合はサインインボタンの代わりにサインアウトできるように UserButton コンポーネントを設定します。サインインした時にのみ表示されるように SignedIn コンポーネントでラップします。サインアウトした後に”/“にリダイレクトされるように afterSignOutUrl props を設定しています。
import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs';
export const NavBar = () => {
return (
<nav className="flex justify-between items-center h-12 mx-4">
<div className="font-bold">Clerk Auth</div>
<SignedOut>
<SignInButton redirectUrl="/dashboard" mode="modal" />
</SignedOut>
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
</nav>
);
};
サインインが行われている場合には UserButton が表示されます。
SignedIn, SignedOut コンポーネントを利用することでサインインした状態、サインしていない状態での要素の表示、非表示の制御を行えることがわかります。
ユーザ情報の表示
App Router を利用している場合はヘルパー関数の auth を利用することで Authentication オブジェクトの情報を取得することができます。
import { auth } from '@clerk/nextjs/server';
export default function DashboardPage() {
console.log(auth());
return (
<div>
<h1>Dashboard</h1>
</div>
);
}
Next.js 14 ではデフォルトですべてのコンポーネントは Server Component としてサーバ側で処理が行われるので開発サーバを実行したターミナルには Authentication オブジェクトの情報が表示されます。ユーザがサインインしている状態か userId の値を利用して分岐処理などを行うことができます。
{
actor: undefined,
sessionClaims: {
azp: 'http://localhost:3000',
exp: 1666622607,
iat: 1666622547,
iss: 'https://clerk.quiet.muskox-85.lcl.dev',
nbf: 1666622537,
sid: 'sess_2GaMqUCB3Sc1WNAkWuNzsnYVVEy',
sub: 'user_2F2u1wtUyUlxKgFkKqtJNtpJJWj'
},
sessionId: 'sess_2GaMqUCB3Sc1WNAkWuNzsnYVVEy',
session: undefined,
userId: 'user_2F2u1wtUyUlxKgFkKqtJNtpJJWj',
user: undefined,
orgId: undefined,
orgRole: undefined,
orgSlug: undefined,
organization: undefined,
getToken: [AsyncFunction (anonymous)],
debug: [Function (anonymous)]
}
User オブジェクトの内容を表示したい場合にはヘルパー関数の currentUser を利用することができます。currentUser 関数を利用する場合は非同期関数の async, await を利用します。Google アカウントを利用している場合は Google で設定しているユーザ名やメールアドレスの情報も含まれます。User オブジェクトを利用することでブラウザ上にユーザ名を表示させることができます。
import { currentUser } from '@clerk/nextjs/server';
export default async function DashboardPage() {
const user = await currentUser();
console.log(user);
return (
<div>
<h1 className="font-bold">Dashboard</h1>
<div>Hello {`${user?.firstName} ${user!.lastName}`}!</div>
</div>
);
}
クライアントコンポーネントでのユーザ情報の取得
ヘルパー関数の auth, currentUser は Server Component で利用することができますが Client Component で利用したい場合には useUser Hook を利用することができます。
Client Component を作成するために components ディレクトリに UserInfo.tsx ファイルを作成して以下のコードを記述します。useUser Hook から戻される値には isSignedIn, isLoaded などがあります。isLoaded を利用しない場合にはページにアクセス直後には isSignedIn の値が false のため”Not signed in”が表示されその後 isSignedIn が true になると”Hello John”が表示されます。
'use client';
import { useUser } from '@clerk/nextjs';
export const UserInfo = () => {
const { isSignedIn, user, isLoaded } = useUser();
if (!isLoaded) {
return null;
}
if (isSignedIn) {
return <div>Hello {user.firstName}!</div>;
}
return <div>Not signed in</div>;
};
‘use client’を設定せず Server Component として useUser Hook を利用した場合にはエラーメッセージが表示されます。
Unhandled Runtime Error
Error: Attempted to call useUser() from the server but useUser is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.
Clerk の基本的な機能しか利用していませんが Clerk を利用することで簡単に Next.js に認証機能が追加できることが理解できたかと思います。