moai-adk-main/.claude/skills/moai-security-auth/SKILL.md
Enterprise Skill for advanced development
npx skillsauth add ajbcoding/claude-skill-eval moai-security-authInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Advanced Authentication with MFA, FIDO2, WebAuthn & Passkeys
Trust Score: 9.8/10 | Version: 4.0.0 | Enterprise Mode | Last Updated: 2025-11-12
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:
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 Standard (2018):
WebAuthn (W3C Standard):
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--------------|
NextAuth.js 5.0.0 (November 2025):
Key Concepts:
Callbacks (Control authentication flow)
authorized: Check if user has accesssignIn: Validate credentials before session creationjwt: Modify JWT payloadsession: Customize session objectProviders (Authentication sources)
Events (Logging & audit trail)
signIn: User logged insignOut: User logged outcreateUser: New user registeredupdateUser: User updatedBasic 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);
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;
}
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 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);
}
);
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;
}
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;
};
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();
}
| 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
content-media
Download YouTube video transcripts when user provides a YouTube URL or asks to download/get/fetch a transcript from YouTube. Also use when user wants to transcribe or get captions/subtitles from a YouTube video.
development
Transform learning content (like YouTube transcripts, articles, tutorials) into actionable implementation plans using the Ship-Learn-Next framework. Use when user wants to turn advice, lessons, or educational content into concrete action steps, reps, or a learning quest.
tools
Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.
tools
Replace with description of the skill and when Claude should use it.