はじめに
新しいサービスを生み出していく際に、はじめから重厚長大な作りをすることは少なくなってきているかと思います。はじめはスピーディにサクッと作りたい、でも大きくなってきたらキッチリ作りたい。「サクッと」から「キッチリ」へ切り替える際に、データ構造がガラッと変わってしまうとサービスを育てていく時の課題になるのではないでしょうか。
(出典)正しいものを正しくつくる
「サクッと」React をはじめとしたフロントエンドを開発する際に、バックエンドには Firebase や Amplify などを選択することが多いかと思います。私も両方とも好きでよく使っています。しかしながら基本的には NoSQL です。RDB で「サクッと」バックエンドを提供してくれるものが Supabase です。
Supabase は「The Open Source Firebase Alternative / オープンソースの Firebase の代替」と自らを謳っており、慣れ親しんだリレーショナル・データベース(PostgreSQL)でありながら使いやすい BaaS(Backend as a Service)なのです。
本稿では、手を動かしながら Supabase を基本から一歩づつ理解していきたいと思います。前回は Row Level Security に触れました。 今回は Auth を深堀りしていきたいと思います。
Supabase とは
読み方は「スーパーベース」と読みます。
主要な機能は以下です。
Auth とは
Supabase Auth は名前の通り、認証と認可の機能を提供しています。Client SDK / JavaScript Client Library を主に利用します。APIエンドポイントも利用できます。
メールアドレス+パスワード、マジックリンク、ワンタイムパスワード (OTP)、ソーシャルログイン、シングルサインオン (SSO) など、多くの一般的な認証方法を使用できます。
Supabase の認証は JSON Web Token (JWT) を基本としています。Auth の機能はデータベース機能と統合されているため Row Level Security (RLS) として認証の範囲を設定できます。要するに Auth を利用するとログインしている人のみに REST API へのアクセスを制限することが可能です。
本稿では取り上げませんが、Next.js のサーバーサイドレンダリング(SSR)と互換性があります。
ユーザーの考え方
Auth スキーマにユーザーとして登録すると、Supabase にアクセスするためのアクセストークンが発行されます。
ユーザーは Permenent users(永続ユーザー) と Anonymouse users(匿名ユーザー) に分かれます。永続ユーザーはログアウト後にも同じユーザーとしてログインできますが、匿名ユーザーは一時的なログインとして利用し、ログアウト後は同じユーザーになることはできません。
ユーザーのデータ型
詳細は公式ドキュメントを参照してください。以下は転記です。
| Attributes | Type | Description(日本語) |
|---|---|---|
| id | string | ユーザーの識別子となる一意なID。 |
| aud | string | オーディエンス(audience)を表すクレーム。 |
| role | string | Postgres の Row Level Security(RLS)チェックに使用されるロールのクレーム。 |
| string | ユーザーのメールアドレス。 | |
| email_confirmed_at | string | ユーザーのメールアドレスが確認された日時。null の場合、メールアドレスは未確認。 |
| phone | string | ユーザーの電話番号。 |
| phone_confirmed_at | string | ユーザーの電話番号が確認された日時。null の場合、電話番号は未確認。 |
| confirmed_at | string | メールアドレスまたは電話番号のいずれかが確認された日時。null の場合、どちらも未確認。 |
| last_sign_in_at | string | ユーザーが最後にサインインした日時。 |
| app_metadata | object | provider 属性は、ユーザーが最初にサインアップした認証プロバイダを示す。providers 属性は、ログインに使用可能なプロバイダの一覧を示す。 |
| user_metadata | object | デフォルトでは最初のプロバイダのアイデンティティ情報が設定されるが、追加のカスタムユーザーメタデータを含めることもできる。identity オブジェクトの詳細は User Identity を参照。情報の順序には依存しないこと。ユーザーが検証なしで編集可能なため、RLS ポリシーや認可ロジックなどのセキュリティ上重要な用途では使用しないこと。 |
| identities | UserIdentity[] | ユーザーに紐づくアイデンティティの配列。 |
| created_at | string | ユーザーが作成された日時。 |
| updated_at | string | ユーザー情報が最後に更新された日時。 |
| is_anonymous | boolean | ユーザーが匿名ユーザーの場合は true。 |
identities にはユーザーの認証方法の情報が入ります。メール、電話、OAuth、SAML認証をサポートしています。
| Attributes | Type | Description(日本語) |
|---|---|---|
| provider_id | string | 認証プロバイダから返されるプロバイダ固有のID。OAuth プロバイダの場合は、その OAuth プロバイダ上のユーザーアカウントIDを指す。email または phone の場合は、auth.users テーブルにおけるユーザーID。 |
| user_id | string | このアイデンティティが紐づいているユーザーのID。 |
| identity_data | object | アイデンティティのメタデータ。OAuth や SAML のアイデンティティの場合、プロバイダから取得したユーザー情報が含まれる。 |
| id | string | アイデンティティを一意に識別するID。 |
| provider | string | 認証プロバイダ名。 |
| string | identity_data 内の任意の email プロパティを参照する生成カラム。 | |
| created_at | string | アイデンティティが作成された日時。 |
| last_sign_in_at | string | このアイデンティティが最後にサインインに使用された日時。 |
| updated_at | string | アイデンティティが最後に更新された日時。 |
(出典)Supabase Doc > Users, Identities
セッションの考え方
JWT に則り、トークンでセッションを管理します。
Supabse Auth の Access Token, Refresh Token を使います。アクセストークンは5分~1時間程度、リフレッシュトークンは一度のみ利用可能です。有効期限はありません。
以下のいずれかの状態になるとログアウトします。
- ユーザーがサインアウトをクリックした場合
- ユーザーがパスワードを変更する、またはセキュリティ上重要な操作を行った場合
- 非アクティブ状態が続き、タイムアウトした場合
- セッションの有効期限(最大存続時間)に達した場合
- ユーザーが別のデバイスでサインインした場合
メールアドレス+パスワード認証
認証方式は他にもありますが、本稿ではメールアドレス+パスワード認証を取り上げます。メールアドレスの認証はデフォルトで有効になっています。
Implicit flow と PKCE flow があります。サーバーサイドレンダリングの場合は PKCE flow でサーバー側での認証を行います。
ユーザーがサインアップをした後、確認リンクがメールに届きます。ユーザーを有効化するとログインできるようになっています。
細かな実装は割愛しますので公式ドキュメントを読んでください。
(出典)Supabse Doc > Password-based
サインアウト
サインアウト機能では、サインアウトのスコープを決めることができます。
- global (default):ユーザーに紐づくすべてのアクティブなセッションを終了する。
- local:現在のセッションのみを終了し、他のデバイスやブラウザでのセッションは継続する。
- others:現在のセッションを除き、その他すべてのセッションを終了する。
// defaults to the global scope
await supabase.auth.signOut()
// sign out from the current session only
await supabase.auth.signOut({ scope: 'local' })
エラーハンドリング
エラーを識別する際は、エラーメッセージの文字列比較ではなく、必ず error.code と error.name を使用してください。HTTP ステータスコードは予期せず変更される可能性があるそうです。
エラーコード表は量が多いので公式ドキュメントを参照してください。
(出典)Supabase Doc > Error Codes
サンプルソースコード
ログイン画面のサンプルです。サインアップ画面はほぼ同じなので割愛します。 formAction={signup} にしてください。
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { login } from "@/features/auth/actions";
import Link from "next/link";
export function LoginForm() {
return (
<div className="flex items-center justify-center min-h-[calc(100vh-100px)]">
<Card className="mx-auto max-w-sm">
<CardHeader>
<CardTitle className="text-2xl">ログイン</CardTitle>
<CardDescription>
メールアドレスとパスワードを入力してアカウントにログインします
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<form className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link
href="#"
className="ml-auto inline-block text-sm underline"
>
パスワードを忘れましたか?
</Link>
</div>
<Input id="password" type="password" name="password" required />
</div>
<Button formAction={login} type="submit" className="w-full">
ログイン
</Button>
</form>
</div>
<div className="mt-4 text-center text-sm">
アカウントをお持ちでないですか?{" "}
<Link href="/signup" className="underline">
サインアップ
</Link>
</div>
</CardContent>
</Card>
</div>
);
}
ログイン、サインアップ、ログアウト処理のサンプル
"use server";
import { createClient } from "@/lib/supabase/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function login(formData: FormData) {
const supabase = await createClient();
// フォームからメールアドレスとパスワードを取得
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// Supabaseでサインイン
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
// エラーがあればリダイレクトしてエラーメッセージを表示
if (error) {
return redirect("/login?message=Could not authenticate user");
}
// パスを再検証してキャッシュをクリア
revalidatePath("/", "layout");
// ログイン後はアカウントページにリダイレクト
redirect("/account");
}
export async function signup(formData: FormData) {
const supabase = await createClient();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const { error } = await supabase.auth.signUp({
email,
password,
options: {
// 確認メール内のリンクをクリックした後のリダイレクト先
emailRedirectTo: `${process.env.NEXT_PUBLIC_BASE_URL}/auth/callback`,
},
});
if (error) {
console.error("Signup error:", error);
return redirect("/login?message=Could not authenticate user");
}
// メール確認を促すメッセージと共にリダイレクト
return redirect("/login?message=Check email to continue sign in process");
}
export async function logout() {
const supabase = await createClient();
await supabase.auth.signOut();
redirect("/");
}
おわりに
以上で Auth 編を終わります。次回は Storage 編です。

