mcpField
M
modern-auth-2026

modern-auth-2026

github.com/curiositechセキュリティ257197 回インストール2 日前に更新
リポジトリ

modern-auth-2026 について

Web・モバイルアプリでパスワードレス認証を構築する開発者向けのスキルです。Passkey、OAuth、アカウント復旧、Supabase Authの設定を、実装例やトラブルシューティングとともに扱います。

主な機能

  1. 01PasskeyとWebAuthnの実装ガイド
  2. 02GoogleとAppleのOAuth設定
  3. 03マジックリンクとOTPのフロー
  4. 04MFAとアカウント復旧のパターン
  5. 05Supabase Authの設定と問題解決

活用シーン

  1. 01WebアプリへのPasskeyログイン追加
  2. 02Supabase Authのソーシャルログイン設定
  3. 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簡素化された OAuth2.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 サインイン

セットアップ要件
  1. Google Cloud Console:

    • OAuth 2.0 クライアント ID(Web アプリケーション)を作成します
    • 許可する JavaScript オリジンに https://yourapp.com を追加します
    • 許可するリダイレクト URI に https://yourapp.supabase.co/auth/v1/callback を追加します
  2. 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 を利用する企業向け/教育向けアプリ

セットアップ要件
  1. Apple Developer Portal:

    • 「Sign in with Apple」機能を有効にします
    • Web 用の Service ID を作成します
    • トークン生成用のキー(.p8 ファイル)を作成します
    • ⚠️ キーは 6 か月ごとに有効期限が切れます。カレンダーにリマインダーを設定してください!
  2. 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 の設定チェックリスト

ダッシュボードの設定
  1. Authentication → Settings:

    • サイト URL:https://yourapp.com
    • リダイレクト URL:有効なコールバックをすべて追加します
    • JWT の有効期限:3600(1 時間)
    • メール確認を有効にする:はい
  2. Authentication → Providers → Email:

    • メールを有効にする:はい
    • メールを確認する:はい(推奨)
    • メールアドレスの変更を保護する:はい
    • メールを二重確認する:いいえ(手間を減らします)
  3. Authentication → Email Templates:

    • すべてのテンプレートをカスタマイズします
    • メールの配信をテストします
    • 適切な有効期限を設定します
  4. 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 ダッシュボードでメール確認が設定されていません

解決策:

  1. Supabase ダッシュボード → Authentication → Providers → Email に移動します
  2. 「Confirm email」が有効になっていることを確認します
  3. メールテンプレートが設定されていることを確認します
  4. SMTP 設定を確認します(または Supabase 内蔵のメール機能を使用します)
  5. 迷惑メールフォルダーを確認します
問題:Apple サインインが突然機能しなくなる

原因: Apple の .p8 キーの有効期限が切れています(6 か月の制限)

解決策:

  1. Apple Developer Portal で新しいキーを生成します
  2. Supabase ダッシュボードでキーを更新します
  3. 次の有効期限に向けてカレンダーにリマインダーを設定します
問題:Google OAuth のリダイレクトエラー

原因: リダイレクト URI が一致していません

解決策:

  1. Google Cloud Console のリダイレクト URI が正確に一致することを確認します:
    • https://yourproject.supabase.co/auth/v1/callback
  2. 末尾のスラッシュがないか確認します
  3. 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 - オープンソースのパスキーサーバー
調査(2026 年)

コメント