moai-security-auth
About
This enterprise skill provides advanced authentication patterns for implementing MFA, FIDO2, WebAuthn, and Passkeys. Use it when building secure session management, migrating to passwordless authentication, or integrating OAuth 2.1 and single sign-on. It covers best practices for NextAuth.js 5.x and Passport.js implementations.
Quick Install
Claude Code
Recommended/plugin add https://github.com/modu-ai/moai-adkgit clone https://github.com/modu-ai/moai-adk.git ~/.claude/skills/moai-security-authCopy and paste this command in Claude Code to install this skill
Documentation
moai-security-auth: Modern Authentication Patterns
Advanced Authentication with MFA, FIDO2, WebAuthn & Passkeys
Trust Score: 9.8/10 | Version: 4.0.0 | Enterprise Mode | Last Updated: 2025-11-12
Overview
Authentication is the foundation of application security. Modern patterns have evolved from passwords to passwordless authentication using FIDO2, WebAuthn, and Passkeys. This Skill covers current best practices for NextAuth.js 5.x, Passport.js, and FIDO2 implementations.
When to use this Skill:
- Implementing secure session management
- Adding multi-factor authentication (MFA)
- Migrating to passwordless authentication (Passkeys/WebAuthn)
- Building FIDO2 hardware key support
- Setting up OAuth 2.1 provider integration
- Implementing JWT session refresh logic
- Building secure single sign-on (SSO) systems
- Managing authentication state in distributed systems
Level 1: Foundations (FREE TIER)
What is Modern Authentication?
Legacy Flow (2010s):
User → Username/Password → Database Hash Comparison → Session Token
Modern Flow (2025):
User → Biometric/Hardware → WebAuthn Server → Cryptographic Verification
OR
User → OAuth Provider → Provider Verification → Access Token + ID Token
Authentication Evolution:
| Era | Method | Security | User Experience |
|---|---|---|---|
| 2000-2010 | Password | Weak | Good |
| 2010-2020 | Password + 2FA | Medium | Poor |
| 2020-2025 | Passwordless | Strong | Excellent |
| 2025+ | Passkeys | Strongest | Best |
FIDO2 & WebAuthn Fundamentals
FIDO2 Standard (2018):
- Published by FIDO Alliance (Google, Microsoft, Apple, etc.)
- Two-factor authentication using hardware keys or biometrics
- No password transmission needed
WebAuthn (W3C Standard):
- Browser API for FIDO2 authentication
- Supports USB keys, biometrics, platform authenticators
- Cryptographic proof instead of password verification
Registration Flow:
User Device Authenticator Relying Party (Server)
| | |
|--Challenge------->| |
| |--User Verification |
| | (Biometric/PIN) |
| | |
|<--Attestation------| |
| | |
|--PublicKey + Attestation------->Verify|Store
Authentication Flow:
User Device Authenticator Relying Party (Server)
| | |
| |<--Challenge------------|
| | |
|--User Verification-| |
| |--Assertion (Signed)-->|Verify Signature
| | |
|<--Authenticated----|<--Success--------------|
Session Management (NextAuth.js 5.x)
NextAuth.js 5.0.0 (November 2025):
- Complete rewrite for Next.js 15
- JWT sessions by default (stateless)
- Built-in OAuth/OIDC provider support
- Passwordless email magic links
Key Concepts:
-
Callbacks (Control authentication flow)
authorized: Check if user has accesssignIn: Validate credentials before session creationjwt: Modify JWT payloadsession: Customize session object
-
Providers (Authentication sources)
- OAuth (Google, GitHub, Microsoft)
- Credentials (username/password)
- Email magic link
- FIDO2/WebAuthn
-
Events (Logging & audit trail)
signIn: User logged insignOut: User logged outcreateUser: New user registeredupdateUser: User updated
Level 2: Intermediate Patterns (STANDARD TIER)
NextAuth.js 5.x Implementation
Basic Setup (JWT Sessions):
// lib/auth.ts
import NextAuth, { type NextAuthConfig } from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Credentials from 'next-auth/providers/credentials';
import { encode as defaultEncode, decode as defaultDecode } from 'next-auth/jwt';
const config = {
providers: [
// 1. OAuth Provider (GitHub)
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
allowDangerousEmailAccountLinking: false
}),
// 2. Passwordless Email Magic Link
Email({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: parseInt(process.env.EMAIL_SERVER_PORT),
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD
}
},
from: process.env.EMAIL_FROM
}),
// 3. Credentials (for custom auth with MFA)
Credentials({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
mfaCode: { label: '2FA Code', type: 'text', optional: true }
},
async authorize(credentials) {
// 1. Verify password
const user = await db.users.findByEmail(credentials.email);
if (!user) return null;
const passwordMatch = await bcrypt.compare(
credentials.password,
user.passwordHash
);
if (!passwordMatch) return null;
// 2. Check MFA if enabled
if (user.mfaEnabled) {
if (!credentials.mfaCode) {
throw new Error('MFA code required');
}
const mfaValid = await verifyTOTP(
credentials.mfaCode,
user.mfaSecret
);
if (!mfaValid) {
throw new Error('Invalid MFA code');
}
}
return {
id: user.id,
email: user.email,
name: user.name
};
}
})
],
// JWT configuration
jwt: {
encode: async (params) => {
// Use RS256 for better security
if (params.token?.mfa === true) {
// Short-lived token for MFA verification
return defaultEncode({ ...params, exp: Date.now() / 1000 + 300 });
}
return defaultEncode(params);
},
decode: defaultDecode
},
// Session configuration
session: {
strategy: 'jwt', // Stateless sessions
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60 // Refresh every 24h
},
// Callbacks for customization
callbacks: {
// Control who can sign in
authorized({ auth, request }) {
const isLoggedIn = !!auth?.user;
const isOnAdminPage = request.nextUrl.pathname.startsWith('/admin');
if (isOnAdminPage) {
return isLoggedIn && auth.user.role === 'admin';
}
return true;
},
// Validate credentials
async signIn({ user, account, profile }) {
// Verify email if required
if (!user.emailVerified) {
throw new Error('Email not verified');
}
// Check if account is locked
if (user.locked) {
throw new Error('Account locked');
}
return true;
},
// Modify JWT token
async jwt({ token, user, account }) {
// Initial sign in
if (user) {
token.id = user.id;
token.role = user.role;
}
// Refresh token data on each session
if (account?.access_token) {
token.accessToken = account.access_token;
token.accessTokenExpires = account.expires_at * 1000;
}
// Check if token needs refresh
if (token.accessTokenExpires &&
Date.now() < token.accessTokenExpires) {
return token;
}
// Refresh access token if expired
return refreshAccessToken(token);
},
// Customize session object
async session({ session, token }) {
session.user.id = token.id;
session.user.role = token.role;
session.accessToken = token.accessToken;
return session;
}
},
// Pages customization
pages: {
signIn: '/auth/signin',
error: '/auth/error',
newUser: '/auth/welcome'
}
} satisfies NextAuthConfig;
export const { handlers, auth, signIn, signOut } = NextAuth(config);
FIDO2/WebAuthn Implementation
Registration (User Setup):
// lib/webauthn.ts
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers/iso';
export async function startRegistration(user: User) {
// 1. Generate challenge
const options = generateRegistrationOptions({
rpID: process.env.WEBAUTHN_RP_ID, // domain.com
rpName: 'My Application',
userID: isoBase64URL.fromBuffer(Buffer.from(user.id)),
userName: user.email,
userDisplayName: user.name,
// 2. Require user verification (biometric/PIN)
authenticatorSelection: {
authenticatorAttachment: 'cross-platform', // USB key
residentKey: 'preferred', // Passkey support
userVerification: 'preferred' // Biometric
},
// 3. Attestation for registration verification
attestationType: 'direct',
// 4. Support multiple algorithms
supportedAlgos: [-7, -257] // ES256, RS256
});
// 3. Store challenge in session (expires in 15 minutes)
await redis.setex(
`webauthn:challenge:${user.id}`,
900,
JSON.stringify(options.challenge)
);
return options;
}
export async function completeRegistration(
user: User,
attestationResponse: PublicKeyCredential
) {
// 1. Get stored challenge
const challengeStr = await redis.get(`webauthn:challenge:${user.id}`);
const challenge = JSON.parse(challengeStr);
// 2. Verify attestation response
const verification = await verifyRegistrationResponse({
response: attestationResponse,
expectedChallenge: challenge,
expectedRPID: process.env.WEBAUTHN_RP_ID,
expectedOrigin: process.env.WEBAUTHN_ORIGIN,
// Verify authenticator is certified
requireResidentKey: false,
requireUserVerification: true
});
if (!verification.verified) {
throw new Error('WebAuthn registration verification failed');
}
// 3. Store public key and counter
await db.webauthnCredentials.create({
user_id: user.id,
credential_id: isoBase64URL.toBuffer(
verification.registrationInfo.credentialID
),
public_key: verification.registrationInfo.credentialPublicKey,
counter: verification.registrationInfo.counter,
transports: attestationResponse.response.getTransports(),
created_at: new Date()
});
// 4. Clean up challenge
await redis.del(`webauthn:challenge:${user.id}`);
return verification;
}
Authentication (Sign In):
import { generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
export async function startAuthentication(email: string) {
// 1. Get user's credentials
const user = await db.users.findByEmail(email);
if (!user) throw new Error('User not found');
const credentials = await db.webauthnCredentials.findByUserId(user.id);
// 2. Generate challenge
const options = generateAuthenticationOptions({
rpID: process.env.WEBAUTHN_RP_ID,
// User must verify with same credential
allowCredentials: credentials.map(cred => ({
id: cred.credential_id,
transports: cred.transports
})),
userVerification: 'required' // Must verify identity
});
// 3. Store challenge for verification
await redis.setex(
`webauthn:auth:${user.id}`,
900,
JSON.stringify(options.challenge)
);
return options;
}
export async function completeAuthentication(
email: string,
assertionResponse: PublicKeyCredential
) {
// 1. Get user
const user = await db.users.findByEmail(email);
// 2. Get credential used
const credentialID = assertionResponse.id;
const credential = await db.webauthnCredentials.findByCredentialID(
Buffer.from(credentialID, 'utf-8')
);
// 3. Get challenge
const challengeStr = await redis.get(`webauthn:auth:${user.id}`);
const challenge = JSON.parse(challengeStr);
// 4. Verify assertion
const verification = await verifyAuthenticationResponse({
response: assertionResponse,
expectedChallenge: challenge,
expectedRPID: process.env.WEBAUTHN_RP_ID,
expectedOrigin: process.env.WEBAUTHN_ORIGIN,
// Verify counter prevents cloning
authenticator: {
credentialID: credential.credential_id,
credentialPublicKey: credential.public_key,
counter: credential.counter
}
});
if (!verification.verified) {
throw new Error('WebAuthn authentication failed');
}
// 5. Update counter (prevents cloning)
await db.webauthnCredentials.update(credential.id, {
counter: verification.authenticationInfo.newCounter
});
return user;
}
Multi-Factor Authentication (TOTP)
Time-based One-Time Password:
import { authenticator } from 'otplib';
import QRCode from 'qrcode';
export async function setupTOTP(user: User) {
// 1. Generate secret
const secret = authenticator.generateSecret({
name: `My App (${user.email})`
});
// 2. Generate QR code
const qrCode = await QRCode.toDataURL(secret);
// 3. Store temporary (not yet verified)
await redis.setex(
`totp:pending:${user.id}`,
600, // 10 minutes
secret
);
return { secret, qrCode };
}
export async function verifyTOTPSetup(user: User, token: string) {
// 1. Get pending secret
const secret = await redis.get(`totp:pending:${user.id}`);
if (!secret) throw new Error('No pending TOTP setup');
// 2. Verify token
const isValid = authenticator.check(token, secret);
if (!isValid) throw new Error('Invalid token');
// 3. Verify backup codes
const backupCodes = Array.from({ length: 10 }).map(() =>
crypto.randomBytes(4).toString('hex').toUpperCase()
);
// 4. Store permanently
await db.users.update(user.id, {
mfaEnabled: true,
mfaSecret: secret,
mfaBackupCodes: backupCodes.map(code =>
bcrypt.hashSync(code, 10)
)
});
// Clean up
await redis.del(`totp:pending:${user.id}`);
return backupCodes;
}
export async function verifyTOTPToken(user: User, token: string) {
// Check if token is backup code
const isBackup = user.mfaBackupCodes.some(hashedCode =>
bcrypt.compareSync(token, hashedCode)
);
if (isBackup) {
// Mark backup code as used
const codes = user.mfaBackupCodes.filter(
code => !bcrypt.compareSync(token, code)
);
await db.users.update(user.id, { mfaBackupCodes: codes });
return true;
}
// Check TOTP token
const isValid = authenticator.check(token, user.mfaSecret);
return isValid;
}
// Rate limit TOTP attempts (prevent brute force)
const totpAttempts = new Map<string, number>();
export async function verifyTOTPWithRateLimit(
user: User,
token: string
) {
const key = `totp:${user.id}`;
const attempts = totpAttempts.get(key) || 0;
if (attempts >= 5) {
throw new Error('Too many failed attempts. Try again in 15 minutes.');
}
const isValid = await verifyTOTPToken(user, token);
if (!isValid) {
totpAttempts.set(key, attempts + 1);
setTimeout(() => totpAttempts.delete(key), 900000); // 15 minutes
throw new Error('Invalid token');
}
totpAttempts.delete(key);
return true;
}
Passport.js with Custom Strategy
Passport.js 0.7.x:
import { Strategy as LocalStrategy } from 'passport-local';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
import bcrypt from 'bcryptjs';
import passport from 'passport';
// 1. Local Strategy (username/password)
passport.use(new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
},
async (req, email, password, done) => {
try {
const user = await db.users.findByEmail(email);
if (!user) {
return done(null, false, {
message: 'Invalid credentials'
});
}
// Check if account is locked
if (user.loginAttempts >= 5 &&
Date.now() < user.lockUntil) {
return done(null, false, {
message: 'Account locked. Try again later.'
});
}
const isPasswordValid = await bcrypt.compare(
password,
user.passwordHash
);
if (!isPasswordValid) {
// Increment failed attempts
await db.users.update(user.id, {
loginAttempts: (user.loginAttempts || 0) + 1,
lockUntil: user.loginAttempts >= 4
? new Date(Date.now() + 30 * 60000) // 30 min lock
: undefined
});
return done(null, false, {
message: 'Invalid credentials'
});
}
// Reset lock on successful login
if (user.loginAttempts > 0) {
await db.users.update(user.id, {
loginAttempts: 0,
lockUntil: null
});
}
return done(null, user);
} catch (err) {
return done(err);
}
}
));
// 2. JWT Strategy
passport.use(new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
algorithms: ['HS256']
},
async (jwtPayload, done) => {
try {
const user = await db.users.findById(jwtPayload.id);
if (!user) {
return done(null, false);
}
// Check if token is blacklisted (logout)
const isBlacklisted = await redis.get(
`jwt:blacklist:${jwtPayload.jti}`
);
if (isBlacklisted) {
return done(null, false);
}
return done(null, user, jwtPayload);
} catch (err) {
return done(err);
}
}
));
// 3. Serialization
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await db.users.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});
// 4. Express middleware
app.post('/login',
passport.authenticate('local', { session: false }),
(req, res) => {
const token = jwt.sign(
{ id: req.user.id, jti: uuid() },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({ token, user: req.user });
}
);
// Protected route
app.get('/profile',
passport.authenticate('jwt', { session: false }),
(req, res) => {
res.json(req.user);
}
);
Level 3: Enterprise Patterns (PREMIUM TIER)
Passkeys (Passwordless Future)
Passkey Registration & Authentication:
// Passkeys = WebAuthn + Backup Sync (iCloud, Google Password Manager)
// Registration same as WebAuthn, but with different UX
export async function registerPasskey(user: User) {
const options = generateRegistrationOptions({
rpID: process.env.WEBAUTHN_RP_ID,
rpName: 'My Application',
userID: isoBase64URL.fromBuffer(Buffer.from(user.id)),
userName: user.email,
userDisplayName: user.name,
// Passkey-specific settings
authenticatorSelection: {
authenticatorAttachment: 'platform', // Device built-in (not USB)
residentKey: 'required', // Passkey must be resident
userVerification: 'required' // Biometric/PIN required
},
attestationType: 'direct'
});
// Passkey will be synced by platform (iCloud, Google, etc.)
return options;
}
// Authenticate with any passkey (phone, laptop, shared device)
export async function authenticateWithPasskey(email: string) {
const user = await db.users.findByEmail(email);
const credentials = await db.webauthnCredentials.findByUserId(
user.id,
{ type: 'passkey' }
);
const options = generateAuthenticationOptions({
rpID: process.env.WEBAUTHN_RP_ID,
allowCredentials: [] // Any passkey works
});
return options;
}
Session Refresh & Sliding Windows
NextAuth.js JWT Refresh:
async function refreshAccessToken(token: JWT) {
try {
// Refresh token with OAuth provider
const response = await fetch(
`https://oauth-provider.com/token`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
grant_type: 'refresh_token',
refresh_token: token.refreshToken
})
}
);
const refreshedTokens = await response.json();
if (!response.ok) throw refreshedTokens;
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken
};
} catch (error) {
return { ...token, error: 'RefreshAccessTokenError' };
}
}
// Sliding window: Extend session if used recently
const jwtCallback = async ({ token, account, user, isNewUser, trigger, session }) => {
if (trigger === 'update' && session?.name) {
token.name = session.name;
}
// Auto-extend session if used in last 24 hours
if (token.exp && Date.now() < token.exp * 1000 - 24 * 60 * 60 * 1000) {
token.exp = Date.now() / 1000 + 30 * 24 * 60 * 60; // +30 days
}
return token;
};
Audit Logging & Suspicious Activity
Authentication Event Logging:
export async function logAuthEvent(
userId: string,
event: string,
metadata: Record<string, any>
) {
const ip = metadata.ip;
const userAgent = metadata.userAgent;
const geoLocation = await getGeolocation(ip);
// Check for suspicious activity
const lastLogin = await db.authLogs
.findLastByUser(userId)
.select('geoLocation', 'timestamp');
const isSuspicious = lastLogin && (
// Login from new country in short time
lastLogin.geoLocation.country !== geoLocation.country &&
Date.now() - lastLogin.timestamp < 3600000 // 1 hour
);
await db.authLogs.create({
user_id: userId,
event,
ip,
userAgent,
geoLocation,
suspicious: isSuspicious,
timestamp: new Date()
});
// Alert user if suspicious
if (isSuspicious) {
await sendEmail(userId, {
template: 'suspicious-login',
data: {
location: geoLocation.city,
timestamp: new Date().toLocaleString()
}
});
}
}
// Middleware to log all auth events
export function authAuditMiddleware(req, res, next) {
const originalSend = res.send;
res.send = function(data) {
if (req.path.includes('/auth/')) {
logAuthEvent(req.user?.id, req.method, {
path: req.path,
status: res.statusCode,
ip: req.ip,
userAgent: req.headers['user-agent']
}).catch(console.error);
}
return originalSend.call(this, data);
};
next();
}
Reference
Official Documentation
- NextAuth.js: https://next-auth.js.org/
- FIDO Alliance: https://fidoalliance.org/
- WebAuthn Specification: https://www.w3.org/TR/webauthn-2/
- SimpleWebAuthn: https://simplewebauthn.dev/
- Passport.js: http://www.passportjs.org/
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- OWASP Password Storage Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
- RFC 4226 (TOTP): https://tools.ietf.org/html/rfc4226
- RFC 6238 (HOTP): https://tools.ietf.org/html/rfc6238
Tools & Libraries (November 2025 Versions)
- next-auth: 5.0.x
- passport: 0.7.x
- @simplewebauthn/server: 10.0.x
- otplib: 12.x
- bcryptjs: 2.4.x
- jsonwebtoken: 9.x
- redis: 5.x
Common Vulnerabilities & Mitigations
| Vulnerability | OWASP | Mitigation |
|---|---|---|
| Weak Password | A02:2021 | TOTP/WebAuthn instead |
| Session Fixation | A02:2021 | Rotate session ID on login |
| Brute Force | A07:2021 | Rate limit + account lockout |
| Token Exposure | A02:2021 | Store in httpOnly cookie |
| Credential Stuffing | A02:2021 | Use bcrypt + salting |
| MFA Bypass | A07:2021 | Enforce MFA verification |
Version: 4.0.0 Enterprise Skill Category: Security (Authentication & Authorization) Complexity: Medium-Advanced Time to Implement: 3-5 hours per component Prerequisites: Node.js, React/Vue.js, OAuth concepts, WebAuthn API
GitHub Repository
Related Skills
sglang
MetaSGLang is a high-performance LLM serving framework that specializes in fast, structured generation for JSON, regex, and agentic workflows using its RadixAttention prefix caching. It delivers significantly faster inference, especially for tasks with repeated prefixes, making it ideal for complex, structured outputs and multi-turn conversations. Choose SGLang over alternatives like vLLM when you need constrained decoding or are building applications with extensive prefix sharing.
evaluating-llms-harness
TestingThis Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.
llamaguard
OtherLlamaGuard is Meta's 7-8B parameter model for moderating LLM inputs and outputs across six safety categories like violence and hate speech. It offers 94-95% accuracy and can be deployed using vLLM, Hugging Face, or Amazon SageMaker. Use this skill to easily integrate content filtering and safety guardrails into your AI applications.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
