認証(Authentication) はアプリケーションを構築する上で重要な機能の一つでどのライブラリ/製品を利用して、どのように設定を行えばいいのか悩んでいる人も多いのではないでしょうか。Next.js の Authentication といえば Auth.js(旧 NextAuth.js)が有名ですが本文書でユーザ認証とユーザ管理を行える Clerk の動作確認を行います。オープンソースの製品ではありませんが設定もシンプルで無料枠もあるので支払い情報の入力なしで利用することができます。利用できるフレームワークは React ベースのフレームワーク(React, Next.js, Remix, RedwoodJS, Gatsby, Expo)と JavaScript です。本文書では Next.js を利用して Clerk の基本的な機能の動作確認を行っています。

Clerk アカウントの作成

Cleak のアカウントの作成を行うためClear のサイトにアクセスします。

Clerkのトップページ
Clerkのトップページ

画面右上の”Sign up”のリンクをクリックします。アカウントの作成画面が表示されるので Github, Google またはメールアドレスを利用してアカウントを作成することができます。

サインアップ画面
サインアップ画面

本文書では Google アカウントを利用してアカウントの作成を行うので”Continue with Google”ボタンをクリックするとアカウントの選択画面が表示されるので利用する Google アカウントを選択してください。

Googleアカウントの選択
Googleアカウントの選択

アプリケーションの追加画面が表示されるので”Add application”をクリックしてください。

アプリケーションの追加画面
アプリケーションの追加画面

Application name には任意の名前のアプリケーション名を入力してユーザのサインアップの方法を選択してください。ここではデフォルトに設定されている”Email address”と Google を選択して”CREATE APPLICATION”ボタンをクリックします。

アプリケーションの作成
アプリケーションの作成

Apple の下に”Show 18 more”というテキストが表示されています。この文字をクリックすると画面に表示されている以外の Provider も表示されます。

プロジェクトの作成が完了してダッシュボード画面が表示されます。これから作成するプロジェクトで利用する API Keys の情報が表示されます。このキーを利用して設定を行います。

ダッシュボード画面とAPI Keys の表示
ダッシュボード画面とAPI Keys の表示

Next.js プロジェクトの作成

動作確認を行う Next.js プロジェクトの作成を行います。プロジェクト名は任意の名前をつけることができますがここでは next-js-clerk という名前にしています。TypeScript, ESLint など利用するかどうか選択することができますが残りの選択肢はすべてデフォルトを選択しています。


 % npx create-next-app@latest
Need to install the following packages:
  create-next-app@13.4.10
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
Creating a new Next.js app in /Users/mac/Desktop/my-nextjs-site/next-js-clerk.

Using npm.

Initializing project with template: app-tw


Installing dependencies:
- react
- react-dom
- next
- typescript
- @types/react
- @types/node
- @types/react-dom
- tailwindcss
- postcss
- autoprefixer
- 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": "^4.23.5",
    "@types/node": "20.5.9",
    "@types/react": "18.2.21",
    "@types/react-dom": "18.2.7",
    "autoprefixer": "10.4.15",
    "eslint": "8.48.0",
    "eslint-config-next": "13.4.19",
    "next": "13.4.19",
    "postcss": "8.4.29",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "tailwindcss": "3.3.3",
    "typescript": "5.2.2"
  }
}

環境変数を保存する.env ファイルを作成して Clerk の管理画面に表示されている API Keys の設定を行います。


NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_YWJvdmUtY2xhbS05Ny5jbGVyay5aY2NvdW50cy5kZXYk
CLERK_SECRET_KEY=sk_test_yE7p3tglzokoPEYU86B1NrN831za3w544yE0VwJzUQ

上記の値は利用できないので各自がアカウントを作成して取得した API Keys を設定してください。

Clerk が管理する認証情報をアプリケーション上で利用できるように app ディレクトリの layout.tsx ファイルに<ClerkProvider>の設定を行う必要があります。


import './globals.css'
import { Inter } from 'next/font/google'
import { ClerkProvider } from '@clerk/nextjs'

const inter = Inter({ subsets: ['latin'] })

export const 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}>{children}</body>
      </html>
    </ClerkProvider>
  )
}

Protect Route

どの URL にアクセスした場合に認証を行うかを決めるために Next.js の Middleware の機能を利用します。src ディレクトリの middleware.ts ファイルを作成して以下のコードを記述します。下記の設定では/api を含めすべてのルート(URL)に対して設定を行っています。


import { authMiddleware } from '@clerk/nextjs';

// This example protects all routes including api/trpc routes
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your middleware
export default authMiddleware({});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

設定が完了したら”npm run dev”コマンドを実行して開発サーバを起動します。


 % npm run dev

> next-js-clerk@0.1.0 dev
> next dev

- info Loaded env from /Users/mac/Desktop/next-js-clerk/.env
- ready started server on [::]:3000, url: http://localhost:3000
- event compiled client and server successfully in 273 ms (20 modules)
- wait compiling...
- event compiled client and server successfully in 143 ms (20 modules)
- info Loaded env from /Users/mac/Desktop/next-js-clerk/.env
- info Loaded env from /Users/mac/Desktop/next-js-clerk/.env

ブラウザから http://localhost:3000 にアクセスを行います。通常は src/page.tsx ファイルに記述された内容がブラウザ上に表示されますが Clerk の設定が行われているので代わりにサインイン画面が表示されます。URL を見ると外部のサイトにリダイレクトされていることがわかります。

サインイン画面表示
サインイン画面表示

開発サーバを起動したターミナルには以下のメッセージが表示されます。

pre data-title=”terminal”> there is no signed-in user, and the path is not included in `ignoredRoutes` or `publicRoutes`. To prevent this behavior, choose one of: 1. To make the route accessible to both signed in and signed out users, pass `publicRoutes: ["/"]` to authMiddleware 2. To prevent Clerk authentication from running at all, pass `ignoredRoutes: ["/((?!api|trpc))(_next|.+\..+)(.*)", "/"]` to authMiddleware 3. Pass a custom `afterAuth` to authMiddleware, and replace Clerk's default behavior of redirecting unless a route is included in publicRoutes For additional information about middleware, please visit https://clerk.com/docs/nextjs/middleware (This log only appears in development mode, or if `debug: true` is passed to authMiddleware)

メッセージの 1 で記述している通り middleware.ts に publicRoutes を設定することで”/“への認証によるアクセスへの制限はなくなります。


import { authMiddleware } from '@clerk/nextjs';

export default authMiddleware({
  publicRoutes: ['/'],
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

設定後に http://localhost:3000/にアクセスするとデフォルトのトップページが表示されます。

Next.jsトップページ
Next.jsトップページ

サインイン、サインアウトページの設定

先ほどアプリケーションにアクセスすると外部のサイトにリダイレクトされサインイン画面が表示されることを確認しました。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 ファイルに以下の環境変数を追加します。


NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/

先ほどの動作確認で middleware.ts ファイルで publicRoutes を追加した場合は削除しておきます。

設定後に http://localhost:3000 にアクセスすると外部のサイトにリダイレクトされずローカルサイトのサインイン画面が表示されます。

ローカルサイトのサインイン画面
ローカルサイトのサインイン画面

ユーザのサインイン

Google アカウントまたはメールアドレスを利用してサインインを行ってみましょう。ここでは Google アカウントを利用するため表示されたサインイン画面で”Continue with Google”ボタンをクリックします。

サインインが完了するとトップページが表示されます。

Next.jsトップページ
Next.jsトップページ

サインインが完了したら Clerk のダッシュボードで Users ページにアクセスします。サインインしたユーザが登録されていることが確認できます。サインインしたユーザは Clerk のダッシュボードで管理することができます。

Clerkのダッシュボードでユーザ一覧を表示
Clerkのダッシュボードでユーザ一覧を表示

ユーザのサインアウト

サインインを行いましたが現在の設定ではサインアウトを行う方法がありません。サインアウトを行うためのボタンもコンポーネントとして準備されているので app/page.tsx ファイルで SignOutButton コンポーネントを import します。


import { SignOutButton } from '@clerk/nextjs';

export default function Home() {
  return (
    <div>
      <SignOutButton />
    </div>
  );
}

スタイルが何も設定されていない Sign Out の文字列が表示されます。button 要素なのでクリックすることができます。

SignOutButtonの設定
SignOutButtonの設定

クリックするとサインアウトすることができます。

表示されるボタンは 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>
  );
}
button要素のカスタマイズ
button要素のカスタマイズ

UserButton の設定

SignOutButton コンポーネントを利用してサインアウトを行うことができましたが UserButton コンポーネントを利用してサインアウトを行うことができます。UserButton コンポーネントはサインアウトだけではなくアカウントの管理も行うことができます。


import { UserButton } from '@clerk/nextjs';

export default function Home() {
  return (
    <div>
      <UserButton afterSignOutUrl="/" />
    </div>
  );
}

UserButton コンポーネントを追加するとボタンが表示されるのでボタンをクリックするとドロップダウンメニューで”Manage account”と”Sign Out”のメニューが表示されます。

UserButtonコンポーネントの表示
UserButtonコンポーネントの表示

“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 します。


export const 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 ファイルではサインインが行われていなくても”/“にアクセスできるように privateRoutes を設定しておきます。


import { authMiddleware } from '@clerk/nextjs';

export default authMiddleware({
  publicRoutes: ['/'],
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

デフォルトで設定されている global.css ファイルのスタイルを削除し、Tailwind CSS の設定のみ残します。


@tailwind base;
@tailwind components;
@tailwind utilities;

ここまでの設定で http://localhost:3000 にアクセスすると Home 画面が表示されます。

トップページ
Homeページ

サインインボタンの設定

”/“ページにサインインボタンを追加してサインインが完了したら Dashboard ページにリダイレクトされるように SignInButton コンポーネントを利用します。redirectUrl 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 redirectUrl="/dashboard" mode="modal" />
    </nav>
  );
};

文字列で”Sign in”ボタンが表示されます。

サインインボタンの表示
サインインボタンの表示

“Sign in”ボタンをクリックして表示されたサインイン画面からサインインを行うと/dashboard にリダイレクトされます。リダイレクトされると Dashboard 画面が表示されます。

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”ボタンが表示されるようになりました。

SignInボタンを非表示
SignInボタンを非表示

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 が表示されます。

UserButtonの表示設定
UserButtonの表示設定

UserButton はサインインしていない場合には表示されないようになっていますがブラウザ上に表示されないだけでその場所には空の div 要素が存在します。SignedIn コンポーネントを利用していない場合にはスタイルの設定によっては UserButton の場所に空の div 要素が存在しているので下記のように表示されます。

UserButton要素の注意点
UserButton要素の注意点

SignedIn, SignedOut コンポーネントを利用することでサインインした状態、サインしていない状態での要素の表示、非表示の制御を行えることがわかります。

ユーザ情報の表示

App Router を利用している場合はヘルパー関数の auth を利用することで Authentication オブジェクトの情報を取得することができます。


import { auth } from '@clerk/nextjs';
export default function DashboardPage() {
  console.log(auth());

  return (
    <div>
      <h1>Dashboard</h1>
    </div>
  );
}

Next.js 13 ではデフォルトですべてのコンポーネントは 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';
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>
  );
}
currentUser関数によるユーザ情報の取得
currentUser関数によるユーザ情報の取得

クライアントコンポーネントでのユーザ情報の取得

ヘルパー関数の 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>;
};
useUser Hookの利用
useUser Hookの利用

‘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 に認証機能が追加できることが理解できたかと思います。