| name | modern-auth-2026 |
| description | 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). |
| allowed-tools | ["Read","Write","Edit","Bash","Grep","Glob","WebSearch","mcp__supabase__*"] |
| metadata | {"category":"DevOps & Site Reliability","tags":["authentication","passkeys","webauthn","oauth","passwordless","supabase","mfa","social-login"],"pairs-with":[{"skill":"oauth-oidc-implementer","reason":"OAuth/OIDC flows are core components of modern auth alongside passkeys and magic links"},{"skill":"supabase-admin","reason":"Supabase Auth provides the backend infrastructure for passkey and OAuth implementation"},{"skill":"security-auditor","reason":"Auth implementation security (token storage, session management) requires dedicated auditing"},{"skill":"cloudflare-worker-dev","reason":"Edge-based auth token validation in Workers is a modern auth deployment pattern"}]} |
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:
{
"@simplewebauthn/browser": "^10.0.0",
"next-passkey-webauthn": "^2.0.0"
}
Backend:
{
"@simplewebauthn/server": "^10.0.0"
}
Next.js Passkey Implementation
1. Database Schema (Supabase):
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[],
device_type text,
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);
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):
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') {
const { data: existingCreds } = await supabase
.from('passkey_credentials')
.select()
.(, user.);
options = ({
: ,
: ,
: user.,
: user.!,
: user.?. || user.!,
: ,
: existingCreds?.( ({
: .(c., ),
: ,
})) || [],
: {
: ,
: ,
: ,
},
});
supabase.().({
: user.,
: options.,
: (.() + * * ),
});
.(options);
}
(step === ) {
{ : challengeData } = supabase
.()
.()
.(, user.)
.();
verification = ({
: credential,
: challengeData!.,
: process..!,
: ,
});
(verification. && verification.) {
{ credentialID, credentialPublicKey, counter } = verification.;
supabase.().({
: user.,
: .(credentialID).(),
: .(credentialPublicKey),
counter,
: credential..,
: verification..,
: verification..,
});
.({ : });
}
.({ : }, { : });
}
}
3. Authentication API Route (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') {
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({
: ,
: ,
: userCredentials. ? userCredentials.( ({
: .(c., ),
: ,
: c.,
})) : ,
});
supabase.().({
: options.,
: options.,
: (.() + * * ),
});
.(options);
}
(step === ) {
credentialId = .(credential., ).();
{ : storedCred } = supabase
.()
.()
.(, credentialId)
.();
(!storedCred) {
.({ : }, { : });
}
{ : challengeData } = supabase
.()
.()
.(, credential...)
.();
verification = ({
: credential,
: challengeData!.,
: process..!,
: ,
: {
: .(storedCred., ),
: storedCred.,
: storedCred.,
},
});
(verification.) {
supabase
.()
.({
: verification..,
: (),
})
.(, storedCred.);
{ : session } = supabase...({
: ,
: storedCred..,
});
.({
: ,
: session.?.
});
}
.({ : }, { : });
}
}
4. Frontend Hook (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 {
const optionsRes = await fetch('/api/passkeys/register', {
method: 'POST',
body: JSON.stringify({ step: 'options' }),
});
const options = await optionsRes.json();
const credential = await startRegistration(options);
const verifyRes = await fetch(, {
: ,
: .({ : , credential }),
});
(!verifyRes.) {
();
}
;
} (: ) {
(err. === ) {
();
} {
(err.);
}
;
} {
();
}
};
= () => {
();
();
{
optionsRes = (, {
: ,
: .({ : , email }),
});
options = optionsRes.();
credential = (options);
verifyRes = (, {
: ,
: .({ : , credential }),
});
(!verifyRes.) {
();
}
{ session } = verifyRes.();
;
} (: ) {
(err. === ) {
();
} {
(err.);
}
;
} {
();
}
};
isSupported = !== &&
. !== ;
{
registerPasskey,
authenticateWithPasskey,
isSupported,
isLoading,
error,
};
}
OAuth: Google Sign-In
Setup Requirements
-
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
-
Supabase Dashboard:
- Authentication → Providers → Google
- Add Client ID and Client Secret
- Enable "Sign in with Google"
Implementation
Supabase Client (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',
prompt: 'consent',
},
},
});
if (error) {
console.error('Google sign-in error:', error);
}
}
Native Mobile (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 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
-
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!
-
Supabase Dashboard:
- Authentication → Providers → Apple
- Add Service ID, Team ID, Key ID
- Upload .p8 key file
Implementation
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);
}
}
Native 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)
guard let identityToken = result.credential.identityToken,
let tokenString = String(data: identityToken, encoding: .utf8) else {
throw AuthError.missingToken
}
try await supabase.auth.signInWithIdToken(
credentials: .init(
provider: .apple,
idToken: tokenString
)
)
}
Magic Links (Email Passwordless)
Best Practices
const { error } = await supabase.auth.signInWithOtp({
email: user.email,
options: {
emailRedirectTo: `${origin}/auth/callback`,
shouldCreateUser: true,
},
});
Email Template Customization
<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>
Recovery Flows
Email Recovery (Password Reset)
await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/update-password`,
});
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
function PromptBackupPasskey() {
const [hasBackup, setHasBackup] = useState(false);
const { data: credentials } = usePasskeyCredentials();
useEffect(() => {
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>
);
}
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:
const options = await generateAuthenticationOptions({
rpID: RP_ID,
authenticatorSelection: {
authenticatorAttachment: undefined,
},
});
Supabase Auth Configuration Checklist
Dashboard Settings
-
Authentication → Settings:
-
Authentication → Providers → Email:
-
Authentication → Email Templates:
-
Authentication → Rate Limiting:
Environment Variables
# 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:
- Go to Supabase Dashboard → Authentication → Providers → Email
- Verify "Confirm email" is enabled
- Check email templates are configured
- Verify SMTP settings (or use Supabase's built-in email)
- Check spam folder
Issue: Apple Sign-In suddenly stops working
Cause: Apple .p8 key expired (6-month limit)
Solution:
- Generate new key in Apple Developer Portal
- Update key in Supabase Dashboard
- Set calendar reminder for next expiry
Issue: Google OAuth redirect error
Cause: Redirect URI mismatch
Solution:
- Verify redirect URI in Google Cloud Console matches exactly:
https://yourproject.supabase.co/auth/v1/callback
- Check for trailing slashes
- Ensure HTTP vs HTTPS matches
Issue: Passkey not syncing between devices
Cause: Credential created with wrong attachment type
Solution:
authenticatorAttachment: 'platform',
Security Best Practices
Token Management
const session = await supabase.auth.getSession();
localStorage.setItem('token', longLivedToken);
Rate Limiting
const rateLimit = {
signIn: { max: 5, windowMs: 15 * 60 * 1000 },
signUp: { max: 3, windowMs: 60 * 60 * 1000 },
passwordReset: { max: 3, windowMs: 60 * 60 * 1000 },
passkey: { max: 10, windowMs: 15 * 60 * 1000 },
};
Secure Defaults
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
});
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');
}
}
References
Official Documentation
Libraries
Research (2026)