🎯

modern-auth-2026

🎯Skill

from erichowens/some_claude_skills

VibeIndex|
What it does

Enables passwordless authentication with passkeys, OAuth, magic links, and multi-factor strategies for modern web applications.

πŸ“¦

Part of

erichowens/some_claude_skills(148 items)

modern-auth-2026

Installation

Add MarketplaceAdd marketplace to Claude Code
/plugin marketplace add erichowens/some_claude_skills
Install PluginInstall plugin from marketplace
/plugin install adhd-design-expert@some-claude-skills
Install PluginInstall plugin from marketplace
/plugin install some-claude-skills@some-claude-skills
git cloneClone repository
git clone https://github.com/erichowens/some_claude_skills.git
Claude Desktop ConfigurationAdd this to your claude_desktop_config.json
{ "mcpServers": { "prompt-learning": { "command": "npx", "args...
πŸ“– Extracted from docs: erichowens/some_claude_skills
13Installs
21
-
Last UpdatedJan 23, 2026

Skill Details

SKILL.md

Modern authentication implementation for 2026 - passkeys (WebAuthn), OAuth (Google, Apple), magic links, and cross-device sync. Use for passwordless-first authentication, social login setup, Supabase Auth, Next.js auth flows, and multi-factor authentication. Activate on "passkeys", "WebAuthn", "Google Sign-In", "Apple Sign-In", "magic link", "passwordless", "authentication", "login", "OAuth", "social login". NOT for session management without auth (use standard JWT docs), authorization/RBAC (use security-auditor), or API key management (use api-architect).

Overview

# Modern Authentication Expert (2026)

Master passwordless-first authentication with passkeys, OAuth, magic links, and cross-device sync for modern web and mobile applications.

When to Use

βœ… USE this skill for:

  • Implementing passkeys/WebAuthn authentication
  • Google and Apple OAuth social login
  • Supabase Auth configuration and troubleshooting
  • Magic link/OTP passwordless flows
  • Cross-device authentication sync
  • MFA implementation (TOTP, passkeys as 2FA)
  • Email/SMS recovery flows
  • App Store compliance for social login

❌ DO NOT use for:

  • Session management without auth context β†’ use standard JWT patterns
  • Authorization/RBAC policies β†’ use security-auditor skill
  • API key management β†’ use api-architect skill
  • Supabase RLS policies β†’ use supabase-admin skill

---

2026 Authentication Landscape

Industry Adoption Stats

  • Passkeys: 87% of US/UK companies now use passkeys (FIDO Alliance)
  • Google: 800+ million accounts use passkeys
  • Amazon: 175 million users created passkeys in first year
  • Trend: Passwordless is the security baseline, not a luxury

Key Standards

| Standard | Purpose | Status |

|----------|---------|--------|

| WebAuthn L2 | Browser passkey API | Fully supported |

| FIDO2/CTAP2 | Cross-platform passkeys | Mature |

| OAuth 2.1 | Simplified OAuth | Replacing 2.0 |

| OAuth3 | Short-lived tokens | Emerging |

| Passkey Sync | iCloud/Google sync | Production |

---

Architecture: Passwordless-First Design

Recommended Auth Hierarchy (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

```

Security Tier Comparison

| Method | Phishing-Resistant | Device-Bound | Sync-Capable | Friction |

|--------|-------------------|--------------|--------------|----------|

| Passkeys | βœ… Yes | βœ… Yes | βœ… Yes | Low |

| Hardware Key | βœ… Yes | βœ… Yes | ❌ No | Medium |

| Google OAuth | ⚠️ Partial | ❌ No | βœ… Yes | Low |

| Apple OAuth | ⚠️ Partial | ❌ No | βœ… Yes | Low |

| Magic Link | ❌ No | ❌ No | βœ… Yes | Medium |

| Email OTP | ❌ No | ❌ No | βœ… Yes | Medium |

| SMS OTP | ❌ No | ❌ No | ❌ No | Medium |

| Password | ❌ No | ❌ No | βœ… Yes | Low |

---

Passkeys (WebAuthn) Implementation

How Passkeys Work

```

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)

```

Library Recommendations

Frontend:

```json

{

"@simplewebauthn/browser": "^10.0.0",

"next-passkey-webauthn": "^2.0.0"

}

```

Backend:

```json

{

"@simplewebauthn/server": "^10.0.0"

}

```

Next.js Passkey Implementation

1. Database Schema (Supabase):

```sql

-- 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. Registration API Route (app/api/passkeys/register/route.ts):

```typescript

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. Authentication API Route (app/api/passkeys/authenticate/route.ts):

```typescript

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. Frontend Hook (hooks/usePasskey.ts):

```typescript

import { startRegistration, startAuthentication } from '@simplewebauthn/browser';

import { useState } from 'react';

export function usePasskey() {

const [isLoading, setIsLoading] = useState(false);

const [error, setError] = useState(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 Sign-In

Setup Requirements

  1. Google Cloud Console:

- Create OAuth 2.0 Client ID (Web application)

- Add authorized JavaScript origins: https://yourapp.com

- Add authorized redirect URIs: https://yourapp.supabase.co/auth/v1/callback

  1. Supabase Dashboard:

- Authentication β†’ Providers β†’ Google

- Add Client ID and Client Secret

- Enable "Sign in with Google"

Implementation

Supabase Client (Next.js):

```typescript

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);

}

}

```

Native Mobile (React Native/Expo):

```typescript

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 Sign-In

App Store Requirements (2024+)

⚠️ Critical Compliance Rule:

> Apps that use third-party login (Google, Facebook, etc.) must also offer an equivalent privacy-focused option. Sign in with Apple satisfies this requirement.

Required if you offer: Google, Facebook, Twitter, Amazon, WeChat login

Exception: Enterprise/education apps with existing SSO

Setup Requirements

  1. Apple Developer Portal:

- Enable "Sign in with Apple" capability

- Create Service ID for web

- Create Key (.p8 file) for token generation

- ⚠️ Key expires every 6 months - set calendar reminder!

  1. Supabase Dashboard:

- Authentication β†’ Providers β†’ Apple

- Add Service ID, Team ID, Key ID

- Upload .p8 key file

Implementation

Web (Supabase):

```typescript

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);

}

}

```

Native iOS (Swift):

```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

)

)

}

```

---

Magic Links (Email Passwordless)

Best Practices

```typescript

// βœ… 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

```

Email Template Customization

```html

Sign in to {{ .SiteURL }}

Click the link below to sign in. This link expires in 10 minutes.

Sign in to Your Account

If you didn't request this, you can safely ignore this email.

```

---

Recovery Flows

Email Recovery (Password Reset)

```typescript

// 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 });

```

Account Recovery Hierarchy

```

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)

```

Implementing Backup Passkeys

```typescript

// 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 (

Add a Backup Passkey

Register a passkey on another device to ensure account recovery.

);

}

```

---

Cross-Device Sync

How Passkey Sync Works

```

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)

```

Cross-Platform Authentication (QR Code)

When user wants to sign in on a device without their passkey:

```typescript

// 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 Configuration Checklist

Dashboard Settings

  1. Authentication β†’ Settings:

- [ ] Site URL: https://yourapp.com

- [ ] Redirect URLs: Add all valid callbacks

- [ ] JWT Expiry: 3600 (1 hour)

- [ ] Enable email confirmations: Yes

  1. Authentication β†’ Providers β†’ Email:

- [ ] Enable Email: Yes

- [ ] Confirm email: Yes (recommended)

- [ ] Secure email change: Yes

- [ ] Double confirm email: No (reduces friction)

  1. Authentication β†’ Email Templates:

- [ ] Customize all templates

- [ ] Test email delivery

- [ ] Set appropriate expiry times

  1. Authentication β†’ Rate Limiting:

- [ ] Email: 3 per hour

- [ ] SMS: 3 per hour

- [ ] Magic links: 3 per 5 minutes

Environment Variables

```env

# 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"

```

---

Common Issues & Solutions

Issue: Sign-up says "Check email" but no email arrives

Cause: Email confirmation not configured in Supabase Dashboard

Solution:

  1. Go to Supabase Dashboard β†’ Authentication β†’ Providers β†’ Email
  2. Verify "Confirm email" is enabled
  3. Check email templates are configured
  4. Verify SMTP settings (or use Supabase's built-in email)
  5. Check spam folder

Issue: Apple Sign-In suddenly stops working

Cause: Apple .p8 key expired (6-month limit)

Solution:

  1. Generate new key in Apple Developer Portal
  2. Update key in Supabase Dashboard
  3. Set calendar reminder for next expiry

Issue: Google OAuth redirect error

Cause: Redirect URI mismatch

Solution:

  1. Verify redirect URI in Google Cloud Console matches exactly:

- https://yourproject.supabase.co/auth/v1/callback

  1. Check for trailing slashes
  2. Ensure HTTP vs HTTPS matches

Issue: Passkey not syncing between devices

Cause: Credential created with wrong attachment type

Solution:

```typescript

// Use 'platform' for synced credentials

authenticatorAttachment: 'platform', // NOT 'cross-platform'

// 'cross-platform' = hardware security keys (no sync)

// 'platform' = device biometrics (sync via iCloud/Google)

```

---

Security Best Practices

Token Management

```typescript

// βœ… 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

```

Rate Limiting

```typescript

// 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 },

};

```

Secure Defaults

```typescript

// 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...

}

```

---

References

Official Documentation

  • [Google Passkeys Developer Guide](https://developers.google.com/identity/passkeys/developer-guides)
  • [Apple Sign in with Apple](https://developer.apple.com/sign-in-with-apple/)
  • [Supabase Auth Documentation](https://supabase.com/docs/guides/auth)
  • [WebAuthn Spec](https://www.w3.org/TR/webauthn-2/)

Libraries

  • [SimpleWebAuthn](https://simplewebauthn.dev/) - Recommended WebAuthn library
  • [Corbado](https://www.corbado.com/blog/supabase-passkeys) - Passkey-as-a-service option
  • [Hanko](https://www.hanko.io/) - Open-source passkey server

Research (2026)

  • [Authentication Trends in 2026](https://www.c-sharpcorner.com/article/authentication-trends-in-2026-passkeys-oauth3-and-webauthn/)
  • [Passwordless & MFA in 2026](https://securityboulevard.com/2025/12/passwordless-mfa-in-2026-passkeys-push-mfa-device-trust/)
  • [FIDO Alliance Passkey Index](https://fidoalliance.org/)