Clerkで"ラクラーク"に認証・認可周りを実装する
はじめに
アプリケーションの開発では、やっぱり認証・認可の部分は辛みが多い部分かなと思います。
SupabaseやFirebaseのAuthenticationやNextAuthなど認証・認可のサービス・ソフトウェアは様々ありますね。
先日のVercel Shipでも発表があったClerkという認証サービスを個人開発に導入してみたのですがかなり開発体験が良く、従来の認証・認可の実装の際にぶち当たる辛みがかなり少なかったので、今回はClerkを使った認証・認可周りの知見を共有できたらと思います。
Clerkとは
Clerkとは、Clerk社が提供するフロントエンドフレームワーク(example: Next.js
, React
)向けの認証・認可サービスです。認証処理の提供のみならず、Clerkが標準で提供する認証情報コンポーネント(例えば、新規登録フォームのコンポーネントである<SignUp />
や、 ユーザ情報を表示するコンポーネントである<UserProfile>
)などがあります。
また諸々の認証周りの設定を管理画面から操作するだけで設定できるため、好き嫌い分かれるかもしれませんが、個人的にはめちゃくちゃいいなと思いました。
とはいえ、Supabase AuthやFirebase Authといった従来の認証サービスと比べて、Clerkは無料枠内のMAU(Monthly Active User)の制限が5,000人と非常に少なく(SupabaseとFirebaseは50,000人)、無料枠で抑えようと思うと個人開発程度の規模でしか使用できないところが少し痛いです。
初期設定
まずはプロジェクトの作成から行なっていきます。
下記のスクショのようにプロジェクトの作成段階からいきなりサービス名の設定だけではなく、認証方法の選択を行うことができます。
任意のサービス名を入力し、認証方法を決定すると下記のように使用するフレームワーク・ライブラリごとに必要な環境変数を提供してくれるため、こちらをコピペしていきます。
上記のスクショのフレームワーク・ライブラリのアイコンを選択し、「CONTINUE IN DOCS」をクリックすると、選択肢したフレームワーク・ライブラリでの諸々の設定等が詳しく記載されたドキュメントに遷移します。
次にセッションのプロバイダを設定します。
ルートのlayout.tsxを<ClerkProvider>
で囲ってあげればOKです。
// app/layout.tsx
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>
)
}
最後に、アクセス制限のためにmiddleware.ts
でauthMiddlewareという@clerk/nextjsのメソッドを呼び出します。(必要であればその中にアクセス制限のコードを記述することができます。)
下記のコードはsrcディレクトリを使用していますが、srcを使わない場合はルートにmiddleware.tsを配置すれば無問題です。
import { authMiddleware } from "@clerk/nextjs";
import { NextResponse } from "next/server";
export default authMiddleware({
afterAuth(auth, req, _evt) {
const signInUrl = new URL("/auth/login", req.url);
// sessionIdおよびuserIdがなければログインページに飛ばす
if (!auth.sessionId && !auth.userId)
return NextResponse.redirect(signInUrl);
},
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
初期設定、非常に簡単でしたね。
以上でClerkの初期設定は完了です🎉
標準コンポーネントのカスタマイズ
まずはClerkが提供する標準のコンポーネントを使用した場合、Clerkでは標準のコンポーネントをプロパティや管理画面からコンポーネントの表示を色々いじることができます。
例えば、<SignUp />
の場合、下記のように「Secured By Clerk」というラベルがついてたりします。実際にサービス開発するとなるとこのラベルを外したいかもしれません。他にも、この新規登録フォーム上に任意のサービスのロゴなどを加えたいかもしれません。
Clerkではそこらへんのカスタマイズも可能です。
例えば、上記で紹介した「Secured By Clerk」というラベルは管理画面から表示を操作することができます。
具体的にはサイドバーのCONFIGURE
→ Customization
→ Branding
と進んでいただき、下記のようにHide Clerk brandingのトグルボタンを有効にするだけで完了です。
他にも使用するコンポーネントや、初期設定時に追加した<ClerkProvider>
のプロパティをいじることで表示を任意に変更することができます。
例えば、以下はSignUpフォームを任意の表示に変更したサンプルコードです。
import { SignUp } from "@clerk/nextjs";
export default function SignupPage() {
return (
<div className="w-[80%] mx-auto">
<>Signup</>
<SignUp
appearance={{
layout: {
helpPageUrl: "https://clerk.dev/support",
logoImageUrl: "/yuz3.JPEG",
logoPlacement: "inside",
socialButtonsPlacement: "bottom",
socialButtonsVariant: "iconButton",
privacyPageUrl: "https://clerk.dev/privacy",
termsPageUrl: "https://clerk.dev/terms",
},
}}
/>
</div>
);
}
上記のプロパティ設定で、以下のスクショのようなフォームを作成することが可能です。
それぞれの設定についてはこんな感じです。
-
helpPageUrl
: 任意のヘルプページを追加できる -
logoImageUrl
: 任意のロゴを設定できる -
logoPlacement
: ロゴの位置およびロゴの有無を設定できる -
socialButtonsPlacement
: ソーシャルボタン(GoogleやGitHun)の位置を設定できる -
socialButtonsVariant
: ソーシャルボタンの種類を設定できる -
privacyPageUrl
: 任意のプライバシーページを追加できる -
termsPageUrl
: 任意の利用規約ページを追加できる
以上の他にもさまざまなカスタマイズが可能です。
公式ドキュメントにカスタマイズについて、詳細に記載されていおりますので、詳しくはそちらをご参照いただければと思います。
認証周りの実装
続いて実際に認証周りの実装をしていきます。
今回は基本的なsignup
, login
, logout
の三つを実装します。
標準コンポーネントを使用する場合
SignUp
新規登録用のコンポーネントはSignUp
を使用します。
以下がサンプルコードです。
import { SignUp } from "@clerk/nextjs";
const SignUpPage = () => (
<SignUp path="/sign-up" routing="path" signInUrl="/sign-in" />
);
export default SignUpPage;
Clerkは非常に優秀で、ログイン状態では、このSignUpなどの未ログインユーザーに対して表示したいコンポーネントは自動で非表示になります。
また、新規登録ページのパスを明示的に設定することが可能です。
設定方法は環境変数にNEXT_PUBLIC_CLERK_SIGN_UP_URL
に対して具体的なパス(例: /signup
)を渡すことで設定できます。例外的に、未ログイン状態の時のトップページにも新規登録フォームをおきたい場合は、上記のサンプルコード二もあるようにSignUpコンポーネントに対してpath
というプロパティをえ渡すことで上書きすることができます。
こんな感じでフォームが表示されます。
LogIn
ログインコンポーネントも新規登録コンポーネントのSignUp
と基本的には同じ要領で実装ができます。
環境変数とSignIn
コンポーネントにpathプロパティを渡せばOKです。
import { SignIn } from "@clerk/nextjs";
const SignInPage = () => (
<SignIn path="/sign-in" routing="path" signUpUrl="/sign-up" />
);
export default SignInPage;
出来上がったログインフォームはこんな感じです。
LogOut
最後にログアウトのコンポーネントについてです。
明示的に<SignOut />
みたいなコンポーネントは見当たりませんでした。
※もしあったらごめんなさい。
自分が確認した限りでは、<UserButton />
というコンポーネントを設置すると、"いい感じ"にユーザー情報だったり、ログアウトボタンを用意してくれます。
例えば、今回自分が個人開発したプロダクトでは、ヘッダにユーザーアイコンを飾りたかったので下記のように実装してみました。
export function Header() {
return (
<ul className="flex items-center gap-3 text-main">
<li>
<ServiceLogo />
</li>
<li>
<UserButton />
</li>
</ul>
);
}
実際にはこんな感じになります。
また、このユーザアイコンをクリックすると下記のようないい感じなメニューモーダルを表示してくれます。
このメニューモーダルにあるsign out
ボタンからログアウトが可能です。
少し外れてしまいますが、上記のメニューモーダルのManage account
ボタンをクリックすると、これまたいい感じなユーザ情報のモーダルを表示してくれます。
このモーダルはCSSクラスを上書きすることで、スタイルの変更も可能です。
.cl-rootBox, .cl-userProfile-root, .cl-card {
border: none;
box-shadow: none;
}
.cl-navbar {
border-right: none;
display: none;
}
.cl-navbarMobileMenuButton {
display: none;
}
.cl-profileSection__activeDevices {
display: none;
}
.cl-headerTitle {
font-size: 1.5rem;
font-weight: 600;
line-height: 2rem;
letter-spacing: 0.025em;
color: #4157D0;
}
.cl-headerSubtitle {
font-size: 0.875rem;
line-height: 1rem;
letter-spacing: 0.025em;
color: #4157D0;
}
Localization対応言語一覧
daDK for da-DK
deDE for de-DE
esES for es-ES
frFR for fr-FR
itIT for it-IT
nlNL for nl-NL
ptBR for pt-BR
ruRU for ru-RU
svSE for sv-SE
enUS for en-US (default)
認証のhooksを使用する場合
Clerkでは、標準のコンポーネントを使用する以外にも、Hooksを利用することで認証フォームの表示を自分好みにカスタマイズすることも可能です。Hooksを利用すれば上記のLocalizationに日本語が未対応である問題も解決できます。
useSignUp
useSignUpメソッドは以下のオブジェクトを返します。
- isLoaded: true;
- signUp: SignUpResource;
- setSession: SetSession;
- setActive: SetActive;
OAuthは管理画面のSocial Connection
から諸々設定可能です。
下記のサンプルコードは、useSignUp()のsignUpオブジェクトを使用してOAuthプロバイダで新規登録する実装例です。
"use client";
import { useSignUp } from "@clerk/nextjs";
import { OAuthStrategy } from "@clerk/nextjs/dist/types/server";
import Link from "next/link";
export function SignupComponent() {
const { signUp } = useSignUp();
const handleSignup = async (strategy: OAuthStrategy) => {
try {
if (signUp) {
await signUp.authenticateWithRedirect({
strategy: strategy,
redirectUrl: "/sso-callback",
redirectUrlComplete: "/",
});
}
} catch (error) {
console.error(error);
}
};
...
useSignIn
useSignIn()はUseSignUp()とほとんど要領が同じなので、少し省略させていただきます。
しかもsignupとsigninで使用しているメソッド名が同じなんですね。
const { signIn } = useSignIn();
const handleLogin = async (strategy: OAuthStrategy) => {
if (signIn) {
await signIn.authenticateWithRedirect({
strategy: strategy,
redirectUrl: "/sso-callback",
redirectUrlComplete: "/",
});
}
};
useClerk
ログアウト部分はuseClerk()というHooksを使用します。
useClerk()というHooksはsignOutメソッド以外にもさまざまな返り値を保持しており、非常に興味深いので、探ってみるといいかもしれません。
const { signOut } = useClerk();
Middleware
Clerkでは、Client Component・Serve Componentのどちらからでも認証情報を取得することができますが、認証情報を取得する際にはmiddlewareを設定している必要があります。
厳密には、auth()
などの認証情報を取得するメソッドを呼び出す部分でauthMiddlewareが適切に設定されていないと下記のようなエラーが発生します。
Error: Clerk: auth() was called but it looks like you aren't using `authMiddleware` in your middleware file.
Please use `authMiddleware` and make sure your middleware matcher is configured correctly and it matches this route or page.
See https://clerk.com/docs/quickstarts/get-started-with-nextjs
そのため、authMiddlewareメソッドを適切に設定する必要があります。
authMiddleware
authMiddlewareメソッドでは以下のプロパティを設定することができます。
プロパティ名 | 型 | 説明 |
---|---|---|
beforeAuth | function | 認証ミドルウェアが実行される前に呼び出される。リダイレクトレスポンスが返された場合、ミドルウェアはそれを尊重し、ユーザーをリダイレクトします。falseが返された場合、認証ミドルウェアは実行されず、リクエストは認証ミドルウェアが存在しないものとして処理されます。 |
afterAuth | function | 認証ミドルウェアの実行後に呼び出されます。この関数はauthオブジェクトへのアクセス権を持ち、authの状態に応じたロジックを実行することができる。 |
publicRoutes | string[] | 認証なしでアクセスできるようにするためのルートのリスト。複数のルートにマッチするパターンや、リクエストオブジェクトにマッチする関数を使用することができます。パスパターンと正規表現がサポートされており、例えば: ['/foo', '/bar(.*)'] や[/^/foo}/.*$/] があります。関数を指定しない限り、サインインとサインアップのURLはデフォルトで含まれます。 |
ignoredRoutes | string[] | ミドルウェアが無視するルートのリストです。このリストには通常、静的ファイルやNext.jsの内部を表すルートが含まれます。パフォーマンスを向上させるために、これらのルートはデフォルトのconfig.matcherを使用してスキップする必要があります。 |
debug | bookean | クラークミドルウェアのデバッグを有効にし、サーバーで詳細をログアウトします。 |
secretKey | string | 実行時に異なる秘密鍵を使用する方法を提供する。 |
publishableKey | string | 実行時に異なる発行可能なキーを使用する方法を提供します。 |
jwtKey | string | 実行時に異なるJWTキーを使用する方法を提供します。 |
公式ドキュメントより引用・翻訳: https://clerk.com/docs/nextjs/middleware
beforeAuth、publicRoutes、afterAuthの実行順序について
基本的にauthMiddlewareのbeforeAuth、publicRoutes、afterAuthは、まさにこの順番で呼び出されます。
beforeAuth、publicRoutesと辿ったのち、afterAuthの有無で処理が異なります。
afterAuthが定義されていない場合、自動的にsignInUrlへのリダイレクトされます。
具体的なランタイムでの、これらのハンドラの挙動は公式が提供している以下の画像が非常にわかりやすいです。
引用:https://clerk.com/docs/nextjs/middleware
Webhookを使用してDBにUser関連のレコードを新規作成する
Clerkでは、基本的に必要な認証情報は兼ね備えていますが、とはいえアプリケーションによってはClerkが保持しないデータを取扱いたい場合などもあるかと思います。
例えば、ユーザーの住所や生年月日といった情報を新規登録の際に入力してもらって、それをUserレコード作成の際にProfileテーブルなどに保存するといったイメージです。
実はClerkでは、上記のようなユースケースも対応可能です。
Clerkではさまざまなイベントをトリガーに発火するWebhookを設定することができます。
Webhookの設定自体は管理画面から可能で、エンドポイントとトリガーとなるイベントを選択できます。今回は、Userレコード作成後にProfileレコードを作成するAPIが呼び出されて欲しいので、user:created
を選択します。
あとはApp DirectoryでAPIを準備したら完了です。
export async function POST(request: Request) {
const { data } = await request.json();
const userId = data.id;
const { data: profile } = await supabase
.from("profiles")
.insert([{ user_id: userId }]);
return new Response(profile);
}
まとめ
実際にサービスの開発にClerkを導入してみて、やはり「認証機能の実装時のつらみが少ない」点が非常に魅力的だなと思いました。
とはいえ、Localizationで日本語対応ができなかったり、MAU数が他の認証サービスと比べても少ない点などまだまだ課題はたくさんありそうですね。
業務で使う機会はまだ先かもしれませんが、個人開発規模であれば、個人的には今後めっちゃ使われるんじゃないかなと予想してます。Vercel Authenticationみたいな感じでVercelとの統合サービスなんかが出たらもっと盛り上がる気もします。
それでは良いClerkライフを!🥳
参考情報
Discussion