modern-auth-2026 について
Web・モバイルアプリでパスワードレス認証を構築する開発者向けのスキルです。Passkey、OAuth、アカウント復旧、Supabase Authの設定を、実装例やトラブルシューティングとともに扱います。
主な機能
- 01PasskeyとWebAuthnの実装ガイド
- 02GoogleとAppleのOAuth設定
- 03マジックリンクとOTPのフロー
- 04MFAとアカウント復旧のパターン
- 05Supabase Authの設定と問題解決
活用シーン
- 01WebアプリへのPasskeyログイン追加
- 02Supabase Authのソーシャルログイン設定
- 03メールまたはSMSによるアカウント復旧の設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill modern-auth-2026インストール後、Claude Code で/modern-auth-2026を入力して読み込まれたか確認します。
同梱ファイル.claude-plugin/plugin.json
詳細
モダン認証エキスパート(2026)
最新のWebアプリケーションやモバイルアプリケーション向けに、パスキー、OAuth、マジックリンク、デバイス間同期を使ったパスワードレス優先の認証を習得します。
使用する場合
✅ このスキルを使用するケース:
- パスキー/WebAuthn 認証の実装
- Google と Apple の OAuth ソーシャルログイン
- Supabase Auth の設定とトラブルシューティング
- マジックリンク/OTP によるパスワードレス認証フロー
- デバイス間の認証同期
- MFA の実装(TOTP、2FA としてのパスキー)
- メール/SMS によるアカウント復旧フロー
- ソーシャルログインに関する App Store のコンプライアンス対応
❌ 次のケースでは使用しないでください:
- 認証コンテキストを伴わないセッション管理 → 標準的な JWT パターンを使用してください
- 認可/RBAC ポリシー →
security-auditorスキルを使用してください - API キーの管理 →
api-architectスキルを使用してください - Supabase RLS ポリシー →
supabase-adminスキルを使用してください
2026 年の認証事情
業界での導入状況
- パスキー: 米国/英国の企業の 87% がパスキーを導入しています(FIDO Alliance)
- Google: 8 億以上のアカウントでパスキーが利用されています
- Amazon: 最初の 1 年間で 1 億 7,500 万人のユーザーがパスキーを作成しました
- トレンド: パスワードレスはぜいたく品ではなく、セキュリティの基本です
主要な標準規格
| 標準規格 | 目的 | 状況 |
|---|---|---|
| WebAuthn L2 | ブラウザー向けパスキー API | 完全にサポート |
| FIDO2/CTAP2 | クロスプラットフォームのパスキー | 成熟 |
| OAuth 2.1 | 簡素化された OAuth | 2.0 に置き換わる規格 |
| OAuth3 | 有効期間の短いトークン | 普及し始めている |
| パスキー同期 | iCloud/Google 同期 | 本番運用中 |
アーキテクチャ:パスワードレス優先の設計
推奨される認証の優先順位(2026 年)
Primary Methods (Phishing-Resistant):
├── 1. Passkeys (WebAuthn) ← PREFERRED
│ ├── Platform authenticators (Face ID, Touch ID, Windows Hello)
│ └── Roaming authenticators (YubiKey, security keys)
├── 2. Social OAuth
│ ├── Google Sign-In (synced passkeys)
│ └── Apple Sign-In (privacy-focused)
│
Fallback Methods (Lower Security):
├── 3. Magic Links (email-based)
├── 4. Email OTP (time-limited codes)
└── 5. SMS OTP (deprecated - SIM swap risk)
⚠️ SMS should be last resort only
Legacy (Avoid):
└── 6. Password + Email ← DISCOURAGE
セキュリティレベルの比較
| 方法 | フィッシング耐性 | デバイスに紐付け | 同期可能 | 手間 |
|---|---|---|---|---|
| パスキー | ✅ はい | ✅ はい | ✅ はい | 低 |
| ハードウェアキー | ✅ はい | ✅ はい | ❌ いいえ | 中 |
| Google OAuth | ⚠️ 部分的 | ❌ いいえ | ✅ はい | 低 |
| Apple OAuth | ⚠️ 部分的 | ❌ いいえ | ✅ はい | 低 |
| マジックリンク | ❌ いいえ | ❌ いいえ | ✅ はい | 中 |
| メール OTP | ❌ いいえ | ❌ いいえ | ✅ はい | 中 |
| SMS OTP | ❌ いいえ | ❌ いいえ | ❌ いいえ | 中 |
| パスワード | ❌ いいえ | ❌ いいえ | ✅ はい | 低 |
パスキー(WebAuthn)の実装
パスキーの仕組み
Registration Flow:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ User │─────▶│ Browser │─────▶│ Server │
│ │ │ WebAuthn │ │ │
└──────────┘ └──────────┘ └──────────┘
│ │ │
│ 1. User clicks │ │
│ "Register" │ │
│ │ 2. Server sends │
│ │◀─ challenge + │
│ │ user info │
│ 3. Device shows │ │
│◀─ biometric │ │
│ │ │
│ 4. User │ │
│─▶ authenticates │ │
│ │ 5. Send public │
│ │─▶ key + signed │
│ │ challenge │
│ │ │
│ │ 6. Server stores│
│ │◀─ public key │
└──────────────────┴──────────────────┘
Key Points:
- Private key NEVER leaves device
- Server only stores public key
- Biometric data stays local
- Credential bound to domain (anti-phishing)
ライブラリの推奨
フロントエンド:
{
"@simplewebauthn/browser": "^10.0.0",
"next-passkey-webauthn": "^2.0.0"
}
バックエンド:
{
"@simplewebauthn/server": "^10.0.0"
}
Next.js でのパスキー実装
1. データベーススキーマ(Supabase):
-- Store passkey credentials
CREATE TABLE passkey_credentials (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
credential_id text UNIQUE NOT NULL,
public_key bytea NOT NULL,
counter integer DEFAULT 0,
transports text[], -- e.g., ['internal', 'hybrid']
device_type text, -- 'platform' or 'cross-platform'
backed_up boolean DEFAULT false,
created_at timestamptz DEFAULT now(),
last_used_at timestamptz
);
CREATE INDEX idx_passkey_user_id ON passkey_credentials(user_id);
CREATE INDEX idx_passkey_credential_id ON passkey_credentials(credential_id);
-- RLS policies
ALTER TABLE passkey_credentials ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read own credentials" ON passkey_credentials
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own credentials" ON passkey_credentials
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own credentials" ON passkey_credentials
FOR DELETE USING (auth.uid() = user_id);
2. 登録 API ルート(app/api/passkeys/register/route.ts):
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
import { createClient } from '@/lib/supabase/server';
const RP_NAME = 'Your App Name';
const RP_ID = process.env.NODE_ENV === 'production'
? 'yourapp.com'
: 'localhost';
export async function POST(request: Request) {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const { step, credential } = await request.json();
if (step === 'options') {
// Get existing credentials to exclude
const { data: existingCreds } = await supabase
.from('passkey_credentials')
.select('credential_id')
.eq('user_id', user.id);
const options = await generateRegistrationOptions({
rpName: RP_NAME,
rpID: RP_ID,
userID: user.id,
userName: user.email!,
userDisplayName: user.user_metadata?.display_name || user.email!,
attestationType: 'none', // For privacy
excludeCredentials: existingCreds?.map(c => ({
id: Buffer.from(c.credential_id, 'base64url'),
type: 'public-key',
})) || [],
authenticatorSelection: {
residentKey: 'preferred', // Discoverable credentials
userVerification: 'preferred', // Biometric when available
authenticatorAttachment: 'platform', // Device-bound (not roaming keys)
},
});
// Store challenge in session (or use signed JWT)
await supabase.from('auth_challenges').upsert({
user_id: user.id,
challenge: options.challenge,
expires_at: new Date(Date.now() + 5 * 60 * 1000), // 5 min
});
return Response.json(options);
}
if (step === 'verify') {
// Get stored challenge
const { data: challengeData } = await supabase
.from('auth_challenges')
.select('challenge')
.eq('user_id', user.id)
.single();
const verification = await verifyRegistrationResponse({
response: credential,
expectedChallenge: challengeData!.challenge,
expectedOrigin: process.env.NEXT_PUBLIC_APP_URL!,
expectedRPID: RP_ID,
});
if (verification.verified && verification.registrationInfo) {
const { credentialID, credentialPublicKey, counter } = verification.registrationInfo;
await supabase.from('passkey_credentials').insert({
user_id: user.id,
credential_id: Buffer.from(credentialID).toString('base64url'),
public_key: Buffer.from(credentialPublicKey),
counter,
transports: credential.response.transports,
device_type: verification.registrationInfo.credentialDeviceType,
backed_up: verification.registrationInfo.credentialBackedUp,
});
return Response.json({ success: true });
}
return Response.json({ error: 'Verification failed' }, { status: 400 });
}
}
3. 認証 API ルート(app/api/passkeys/authenticate/route.ts):
import { generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
import { createClient } from '@/lib/supabase/server';
export async function POST(request: Request) {
const supabase = createClient();
const { step, credential, email } = await request.json();
if (step === 'options') {
// For discoverable credentials, email is optional
let userCredentials = [];
if (email) {
const { data: user } = await supabase
.from('profiles')
.select('id')
.eq('email', email)
.single();
if (user) {
const { data: creds } = await supabase
.from('passkey_credentials')
.select('credential_id, transports')
.eq('user_id', user.id);
userCredentials = creds || [];
}
}
const options = await generateAuthenticationOptions({
rpID: RP_ID,
userVerification: 'preferred',
allowCredentials: userCredentials.length ? userCredentials.map(c => ({
id: Buffer.from(c.credential_id, 'base64url'),
type: 'public-key',
transports: c.transports,
})) : undefined, // Empty = discoverable credential flow
});
// Store challenge
await supabase.from('auth_challenges').upsert({
challenge_id: options.challenge,
challenge: options.challenge,
expires_at: new Date(Date.now() + 5 * 60 * 1000),
});
return Response.json(options);
}
if (step === 'verify') {
// Find credential
const credentialId = Buffer.from(credential.id, 'base64url').toString('base64url');
const { data: storedCred } = await supabase
.from('passkey_credentials')
.select('*, profiles!inner(email)')
.eq('credential_id', credentialId)
.single();
if (!storedCred) {
return Response.json({ error: 'Credential not found' }, { status: 401 });
}
// Get challenge
const { data: challengeData } = await supabase
.from('auth_challenges')
.select('challenge')
.eq('challenge_id', credential.response.clientDataJSON.challenge)
.single();
const verification = await verifyAuthenticationResponse({
response: credential,
expectedChallenge: challengeData!.challenge,
expectedOrigin: process.env.NEXT_PUBLIC_APP_URL!,
expectedRPID: RP_ID,
authenticator: {
credentialID: Buffer.from(storedCred.credential_id, 'base64url'),
credentialPublicKey: storedCred.public_key,
counter: storedCred.counter,
},
});
if (verification.verified) {
// Update counter
await supabase
.from('passkey_credentials')
.update({
counter: verification.authenticationInfo.newCounter,
last_used_at: new Date(),
})
.eq('id', storedCred.id);
// Create Supabase session
const { data: session } = await supabase.auth.admin.generateLink({
type: 'magiclink',
email: storedCred.profiles.email,
});
return Response.json({
success: true,
session: session.properties?.hashed_token
});
}
return Response.json({ error: 'Verification failed' }, { status: 401 });
}
}
4. フロントエンドフック(hooks/usePasskey.ts):
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
import { useState } from 'react';
export function usePasskey() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const registerPasskey = async () => {
setIsLoading(true);
setError(null);
try {
// Get options from server
const optionsRes = await fetch('/api/passkeys/register', {
method: 'POST',
body: JSON.stringify({ step: 'options' }),
});
const options = await optionsRes.json();
// Start WebAuthn registration
const credential = await startRegistration(options);
// Verify with server
const verifyRes = await fetch('/api/passkeys/register', {
method: 'POST',
body: JSON.stringify({ step: 'verify', credential }),
});
if (!verifyRes.ok) {
throw new Error('Verification failed');
}
return true;
} catch (err: any) {
// Handle user cancellation gracefully
if (err.name === 'NotAllowedError') {
setError('Passkey registration cancelled');
} else {
setError(err.message);
}
return false;
} finally {
setIsLoading(false);
}
};
const authenticateWithPasskey = async (email?: string) => {
setIsLoading(true);
setError(null);
try {
const optionsRes = await fetch('/api/passkeys/authenticate', {
method: 'POST',
body: JSON.stringify({ step: 'options', email }),
});
const options = await optionsRes.json();
const credential = await startAuthentication(options);
const verifyRes = await fetch('/api/passkeys/authenticate', {
method: 'POST',
body: JSON.stringify({ step: 'verify', credential }),
});
if (!verifyRes.ok) {
throw new Error('Authentication failed');
}
const { session } = await verifyRes.json();
// Exchange for Supabase session
// ...
return true;
} catch (err: any) {
if (err.name === 'NotAllowedError') {
setError('Passkey authentication cancelled');
} else {
setError(err.message);
}
return false;
} finally {
setIsLoading(false);
}
};
const isSupported = typeof window !== 'undefined' &&
window.PublicKeyCredential !== undefined;
return {
registerPasskey,
authenticateWithPasskey,
isSupported,
isLoading,
error,
};
}
OAuth:Google サインイン
セットアップ要件
-
Google Cloud Console:
- OAuth 2.0 クライアント ID(Web アプリケーション)を作成します
- 許可する JavaScript オリジンに
https://yourapp.comを追加します - 許可するリダイレクト URI に
https://yourapp.supabase.co/auth/v1/callbackを追加します
-
Supabase ダッシュボード:
- Authentication → Providers → Google
- クライアント ID とクライアントシークレットを追加します
- 「Sign in with Google」を有効にします
実装
Supabase クライアント(Next.js):
import { createClient } from '@/lib/supabase/client';
async function signInWithGoogle() {
const supabase = createClient();
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
access_type: 'offline', // For refresh tokens
prompt: 'consent', // Force consent screen
},
},
});
if (error) {
console.error('Google sign-in error:', error);
}
}
ネイティブモバイル(React Native/Expo):
import * as Google from 'expo-auth-session/providers/google';
import { createClient } from '@supabase/supabase-js';
export function useGoogleAuth() {
const [request, response, promptAsync] = Google.useIdTokenAuthRequest({
clientId: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID,
iosClientId: process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID,
androidClientId: process.env.EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID,
});
useEffect(() => {
if (response?.type === 'success') {
const { id_token } = response.params;
supabase.auth.signInWithIdToken({
provider: 'google',
token: id_token,
});
}
}, [response]);
return { signIn: () => promptAsync(), isLoading: !request };
}
OAuth:Apple サインイン
App Store の要件(2024 年以降)
⚠️ 重要なコンプライアンスルール:
サードパーティーのログイン(Google、Facebook など)を使用するアプリは、同等のプライバシー重視の選択肢も提供する必要があります。Apple でサインインは、この要件を満たします。
次を提供する場合に必須: Google、Facebook、Twitter、Amazon、WeChat のログイン
例外: 既存の SSO を利用する企業向け/教育向けアプリ
セットアップ要件
-
Apple Developer Portal:
- 「Sign in with Apple」機能を有効にします
- Web 用の Service ID を作成します
- トークン生成用のキー(.p8 ファイル)を作成します
- ⚠️ キーは 6 か月ごとに有効期限が切れます。カレンダーにリマインダーを設定してください!
-
Supabase ダッシュボード:
- Authentication → Providers → Apple
- Service ID、Team ID、Key ID を追加します
- .p8 キーファイルをアップロードします
実装
Web(Supabase):
async function signInWithApple() {
const supabase = createClient();
const { error } = await supabase.auth.signInWithOAuth({
provider: 'apple',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
if (error) {
console.error('Apple sign-in error:', error);
}
}
ネイティブ iOS(Swift):
import AuthenticationServices
func handleAppleSignIn() async throws {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let result = try await performSignIn(request)
// Extract ID token
guard let identityToken = result.credential.identityToken,
let tokenString = String(data: identityToken, encoding: .utf8) else {
throw AuthError.missingToken
}
// Sign in to Supabase
try await supabase.auth.signInWithIdToken(
credentials: .init(
provider: .apple,
idToken: tokenString
)
)
}
マジックリンク(メールによるパスワードレス認証)
ベストプラクティス
// ✅ Good: Short TTL, single-use
const { error } = await supabase.auth.signInWithOtp({
email: user.email,
options: {
emailRedirectTo: `${origin}/auth/callback`,
shouldCreateUser: true, // Auto-create on first login
},
});
// Configure in Supabase Dashboard:
// - Magic Link expiry: 5-10 minutes (shorter is safer)
// - Rate limit: 3 per hour per email
メールテンプレートのカスタマイズ
<h2>Sign in to {{ .SiteURL }}</h2>
<p>Click the link below to sign in. This link expires in 10 minutes.</p>
<p><a href="{{ .ConfirmationURL }}">Sign in to Your Account</a></p>
<p>If you didn't request this, you can safely ignore this email.</p>
リカバリーフロー
メールによるリカバリー(パスワードのリセット)
// Request reset
await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/update-password`,
});
// Update password (on /auth/update-password page)
await supabase.auth.updateUser({ password: newPassword });
アカウントリカバリーの階層
Recovery Options (in order of security):
1. Backup Passkey (stored on different device)
2. Trusted Recovery Contact (delegated access)
3. Email verification + Security questions
4. Email-only recovery (last resort)
5. SMS recovery ⚠️ (vulnerable to SIM swap)
バックアップ用パスキーの実装
// Prompt user to register backup device after primary
function PromptBackupPasskey() {
const [hasBackup, setHasBackup] = useState(false);
const { data: credentials } = usePasskeyCredentials();
useEffect(() => {
// Check if user has only one passkey
if (credentials?.length === 1) {
setHasBackup(false);
}
}, [credentials]);
if (hasBackup) return null;
return (
<div className="bg-amber-50 border border-amber-200 p-4 rounded-lg">
<h3>Add a Backup Passkey</h3>
<p>Register a passkey on another device to ensure account recovery.</p>
<Button onClick={registerPasskey}>Add Backup Device</Button>
</div>
);
}
クロスデバイス同期
パスキーの同期の仕組み
Device A (iPhone) iCloud Keychain Device B (Mac)
┌─────────────────┐ ┌─────────────┐ ┌─────────────────┐
│ Create Passkey │──────────▶│ E2E Encrypt │──────────▶│ Passkey Ready │
│ for example.com │ │ & Sync │ │ to use │
└─────────────────┘ └─────────────┘ └─────────────────┘
Google Password Manager:
- Android devices synced
- Chrome browser synced
- Windows via Chrome
Apple iCloud Keychain:
- All Apple devices synced
- Safari on all platforms
- Shared with Family Sharing (optional)
クロスプラットフォーム認証(QR コード)
ユーザーがパスキーを持たないデバイスでサインインする場合:
// Device A shows QR code
// User scans with phone (Device B) that has passkey
// Phone authenticates via Bluetooth proximity
// This is handled automatically by the browser's WebAuthn implementation
// No additional code needed - just allow hybrid transports:
const options = await generateAuthenticationOptions({
rpID: RP_ID,
authenticatorSelection: {
// Allow cross-device (QR code) authentication
authenticatorAttachment: undefined, // Don't restrict
},
});
Supabase Auth の設定チェックリスト
ダッシュボードの設定
-
Authentication → Settings:
- サイト URL:
https://yourapp.com - リダイレクト URL:有効なコールバックをすべて追加します
- JWT の有効期限:3600(1 時間)
- メール確認を有効にする:はい
- サイト URL:
-
Authentication → Providers → Email:
- メールを有効にする:はい
- メールを確認する:はい(推奨)
- メールアドレスの変更を保護する:はい
- メールを二重確認する:いいえ(手間を減らします)
-
Authentication → Email Templates:
- すべてのテンプレートをカスタマイズします
- メールの配信をテストします
- 適切な有効期限を設定します
-
Authentication → Rate Limiting:
- メール:1 時間あたり 3 回
- SMS:1 時間あたり 3 回
- マジックリンク:5 分あたり 3 回
環境変数
# Required
NEXT_PUBLIC_SUPABASE_URL=https://yourproject.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-key
# Google OAuth
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
# Apple OAuth
APPLE_SERVICE_ID=your-service-id
APPLE_TEAM_ID=your-team-id
APPLE_KEY_ID=your-key-id
APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."
# Passkeys
PASSKEY_RP_ID=yourapp.com
PASSKEY_RP_NAME="Your App Name"
よくある問題と解決策
問題:「メールを確認してください」と表示されるが、メールが届かない
原因: Supabase ダッシュボードでメール確認が設定されていません
解決策:
- Supabase ダッシュボード → Authentication → Providers → Email に移動します
- 「Confirm email」が有効になっていることを確認します
- メールテンプレートが設定されていることを確認します
- SMTP 設定を確認します(または Supabase 内蔵のメール機能を使用します)
- 迷惑メールフォルダーを確認します
問題:Apple サインインが突然機能しなくなる
原因: Apple の .p8 キーの有効期限が切れています(6 か月の制限)
解決策:
- Apple Developer Portal で新しいキーを生成します
- Supabase ダッシュボードでキーを更新します
- 次の有効期限に向けてカレンダーにリマインダーを設定します
問題:Google OAuth のリダイレクトエラー
原因: リダイレクト URI が一致していません
解決策:
- Google Cloud Console のリダイレクト URI が正確に一致することを確認します:
https://yourproject.supabase.co/auth/v1/callback
- 末尾のスラッシュがないか確認します
- HTTP と HTTPS の設定が一致していることを確認します
問題:パスキーがデバイス間で同期されない
原因: 誤ったアタッチメントタイプで認証情報が作成されています
解決策:
// Use 'platform' for synced credentials
authenticatorAttachment: 'platform', // NOT 'cross-platform'
// 'cross-platform' = hardware security keys (no sync)
// 'platform' = device biometrics (sync via iCloud/Google)
セキュリティのベストプラクティス
トークン管理
// ✅ Good: Short-lived access tokens + refresh
const session = await supabase.auth.getSession();
// Access token: 1 hour
// Refresh token: 7 days (rotate on use)
// ✅ Good: Secure token storage
// Browser: HttpOnly cookies (Supabase handles this)
// Mobile: Secure Keychain/Keystore
// ❌ Bad: Long-lived tokens in localStorage
localStorage.setItem('token', longLivedToken); // DON'T
レート制限
// Implement rate limiting on auth endpoints
const rateLimit = {
signIn: { max: 5, windowMs: 15 * 60 * 1000 }, // 5 per 15 min
signUp: { max: 3, windowMs: 60 * 60 * 1000 }, // 3 per hour
passwordReset: { max: 3, windowMs: 60 * 60 * 1000 },
passkey: { max: 10, windowMs: 15 * 60 * 1000 },
};
安全なデフォルト設定
// Always verify email on signup
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
// Supabase will only create confirmed user after email click
},
});
// Require email verification for sensitive actions
async function sensitiveAction(userId: string) {
const { data: user } = await supabase.auth.getUser();
if (!user?.email_confirmed_at) {
throw new Error('Please verify your email first');
}
// Proceed with action...
}
参考資料
公式ドキュメント
ライブラリ
- SimpleWebAuthn - 推奨される WebAuthn ライブラリ
- Corbado - サービスとしてのパスキーの選択肢
- Hanko - オープンソースのパスキーサーバー