Claude Code Plugins

Community-maintained marketplace

Feedback

moai-security-auth

@jg-chalk-io/Nora-LiveKit
0
0

Modern authentication patterns with MFA, FIDO2, WebAuthn & Passkeys

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name moai-security-auth
version 4.0.0
status stable
updated Thu Nov 20 2025 00:00:00 GMT+0000 (Coordinated Universal Time)
description Modern authentication patterns with MFA, FIDO2, WebAuthn & Passkeys
category Security
allowed-tools Read, Bash, WebSearch, WebFetch

moai-security-auth: Modern Authentication Patterns

Advanced authentication with MFA, FIDO2, WebAuthn & Passkeys

Trust Score: 9.8/10 | Version: 4.0.0


Overview

Enterprise authentication expert covering modern security patterns:

  • Passwordless Authentication: FIDO2, WebAuthn, and Passkeys
  • Multi-Factor Authentication: TOTP, SMS, and hardware tokens
  • OAuth 2.1 Integration: Social login and enterprise SSO
  • Session Management: JWT, refresh tokens, and secure cookies
  • Advanced Security: Rate limiting, account lockout, and audit logging

Core Technologies:

  • NextAuth.js 5.x for Next.js applications
  • Passport.js for Express.js applications
  • WebAuthn API for passwordless authentication
  • JWT for stateless session management

Authentication Evolution

Era Method Security UX
2000-2010 Password Weak Good
2010-2020 Password + 2FA Medium Poor
2020-2025 Passwordless Strong Excellent
2025+ Passkeys Strongest Best

NextAuth.js 5.x Implementation

Complete Configuration

// lib/auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { DrizzleAdapter } from "@auth/drizzle-adapter";

export const config = {
  adapter: DrizzleAdapter(db),
  providers: [
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),

    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
        mfaCode: { label: "2FA Code", type: "text", optional: true },
      },
      async authorize(credentials) {
        const user = await db.query.users.findFirst({
          where: eq(users.email, credentials.email),
        });

        if (!user) return null;

        // Verify password
        const valid = await bcrypt.compare(
          credentials.password,
          user.passwordHash
        );

        if (!valid) return null;

        // Verify MFA if enabled
        if (user.mfaEnabled && user.mfaSecret) {
          if (!credentials.mfaCode) {
            throw new Error("MFA code required");
          }

          const mfaValid = speakeasy.totp.verify({
            secret: user.mfaSecret,
            encoding: "base32",
            token: credentials.mfaCode,
          });

          if (!mfaValid) throw new Error("Invalid MFA code");
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
        };
      },
    }),
  ],

  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },

  callbacks: {
    async authorized({ auth, request }) {
      const isLoggedIn = !!auth?.user;
      const isAdmin = auth?.user?.role === "admin";
      const isAdminRoute = request.nextUrl.pathname.startsWith("/admin");

      return isAdminRoute ? isLoggedIn && isAdmin : isLoggedIn;
    },

    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.role = user.role;
      }
      return token;
    },

    async session({ session, token }) {
      if (token) {
        session.user.id = token.id;
        session.user.role = token.role;
      }
      return session;
    },
  },

  pages: {
    signIn: "/auth/signin",
    error: "/auth/error",
  },
};

export const { handlers, auth, signIn, signOut } = NextAuth(config);

MFA Implementation

TOTP Setup

// lib/mfa.ts
import speakeasy from "speakeasy";
import QRCode from "qrcode";

export class MFAService {
  static generateSecret(userEmail: string) {
    return speakeasy.generateSecret({
      name: `MyApp (${userEmail})`,
      issuer: "MyApp",
    });
  }

  static async generateQRCode(secret: speakeasy.GeneratedSecret) {
    const otpauthUrl = speakeasy.otpauthURL({
      secret: secret.base32,
      label: secret.name,
      issuer: secret.issuer,
    });

    return await QRCode.toDataURL(otpauthUrl);
  }

  static verifyToken(secret: string, token: string): boolean {
    return speakeasy.totp.verify({
      secret,
      encoding: "base32",
      token,
      window: 1, // Allow time drift
    });
  }

  static async enableMFA(userId: string, secret: string, token: string) {
    if (!this.verifyToken(secret, token)) {
      throw new Error("Invalid verification code");
    }

    await db
      .update(users)
      .set({
        mfaEnabled: true,
        mfaSecret: secret,
        mfaEnabledAt: new Date(),
      })
      .where(eq(users.id, userId));
  }
}

MFA API Routes

// app/api/auth/mfa/enable/route.ts
import { auth } from "@/lib/auth";
import { MFAService } from "@/lib/mfa";

export async function POST(request: Request) {
  const session = await auth();
  if (!session?.user?.id) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { token } = await request.json();
  const secret = MFAService.generateSecret(session.user.email);

  await MFAService.enableMFA(session.user.id, secret.base32, token);

  return Response.json({ success: true });
}

WebAuthn & Passkeys

WebAuthn Service

// lib/webauthn.ts
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from "@simplewebauthn/server";

const rpID = process.env.WEBAUTHN_RP_ID!;
const rpName = "MyApp";
const origin = process.env.WEBAUTHN_ORIGIN!;

export class WebAuthnService {
  static async startRegistration(userId: string, email: string, name: string) {
    const options = generateRegistrationOptions({
      rpID,
      rpName,
      userID: new TextEncoder().encode(userId),
      userName: email,
      userDisplayName: name,
      authenticatorSelection: {
        residentKey: "preferred", // Passkey support
        userVerification: "preferred", // Biometric
      },
    });

    // Store challenge in session
    await redis.setex(
      `webauthn:register:${userId}`,
      900, // 15 minutes
      JSON.stringify(options.challenge)
    );

    return options;
  }

  static async completeRegistration(userId: string, response: any) {
    const challengeStr = await redis.get(`webauthn:register:${userId}`);
    if (!challengeStr) {
      throw new Error("Registration challenge expired");
    }

    const expectedChallenge = JSON.parse(challengeStr);
    const verification = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin: origin,
      expectedRPID: rpID,
      requireUserVerification: true,
    });

    if (!verification.verified || !verification.registrationInfo) {
      throw new Error("Registration verification failed");
    }

    // Store credential
    await db.webauthnCredentials.create({
      userId,
      credentialId: verification.registrationInfo.credentialID,
      credentialPublicKey: verification.registrationInfo.credentialPublicKey,
      counter: verification.registrationInfo.counter,
      transports: response.response.transports,
    });

    await redis.del(`webauthn:register:${userId}`);

    return verification;
  }

  static async startAuthentication(email: string) {
    const user = await db.query.users.findFirst({
      where: eq(users.email, email),
      with: { credentials: true },
    });

    if (!user) throw new Error("User not found");

    const options = generateAuthenticationOptions({
      rpID,
      allowCredentials: user.credentials.map((cred) => ({
        id: cred.credentialId,
        type: "public-key",
        transports: cred.transports,
      })),
      userVerification: "preferred",
    });

    await redis.setex(
      `webauthn:auth:${user.id}`,
      900,
      JSON.stringify(options.challenge)
    );

    return options;
  }

  static async completeAuthentication(email: string, response: any) {
    const user = await db.query.users.findFirst({
      where: eq(users.email, email),
      with: { credentials: true },
    });

    if (!user) throw new Error("User not found");

    const challengeStr = await redis.get(`webauthn:auth:${user.id}`);
    if (!challengeStr) {
      throw new Error("Authentication challenge expired");
    }

    const expectedChallenge = JSON.parse(challengeStr);
    const credential = user.credentials.find(
      (cred) => Buffer.compare(cred.credentialId, response.id) === 0
    );

    if (!credential) throw new Error("Credential not found");

    const verification = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin: origin,
      expectedRPID: rpID,
      credential: {
        id: credential.credentialId,
        publicKey: credential.credentialPublicKey,
        counter: credential.counter,
        transports: credential.transports,
      },
      requireUserVerification: true,
    });

    if (!verification.verified) {
      throw new Error("Authentication verification failed");
    }

    // Update counter
    await db.webauthnCredentials
      .update({
        counter: verification.authenticationInfo.newCounter,
      })
      .where(eq(webauthnCredentials.id, credential.id));

    await redis.del(`webauthn:auth:${user.id}`);

    return user;
  }
}

Security Middleware

Rate Limiting

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const rateLimit = new Map<string, { count: number; resetTime: number }>();

export async function middleware(request: NextRequest) {
  const ip = request.ip || "unknown";

  // Rate limit auth endpoints
  if (request.nextUrl.pathname.startsWith("/api/auth/")) {
    const now = Date.now();
    const windowMs = 15 * 60 * 1000; // 15 minutes
    const maxRequests = 5;

    const record = rateLimit.get(ip);

    if (!record || now > record.resetTime) {
      rateLimit.set(ip, {
        count: 1,
        resetTime: now + windowMs,
      });
    } else {
      record.count++;

      if (record.count > maxRequests) {
        return NextResponse.json(
          { error: "Too many requests" },
          { status: 429, headers: { "Retry-After": "60" } }
        );
      }
    }
  }

  return NextResponse.next();
}

Account Lockout

// lib/auth-security.ts
export class AuthSecurityService {
  private static readonly MAX_ATTEMPTS = 5;
  private static readonly LOCKOUT_DURATION = 15 * 60 * 1000;

  static async checkLockout(identifier: string): Promise<boolean> {
    const attempts = await redis.get(`auth:attempts:${identifier}`);
    if (!attempts) return false;

    const { count, lockUntil } = JSON.parse(attempts);

    if (count >= this.MAX_ATTEMPTS && Date.now() < lockUntil) {
      return true;
    }

    if (Date.now() > lockUntil) {
      await redis.del(`auth:attempts:${identifier}`);
    }

    return false;
  }

  static async recordFailedAttempt(identifier: string): Promise<void> {
    const key = `auth:attempts:${identifier}`;
    const current = await redis.get(key);

    if (!current) {
      await redis.setex(
        key,
        this.LOCKOUT_DURATION / 1000,
        JSON.stringify({
          count: 1,
          lockUntil: Date.now() + this.LOCKOUT_DURATION,
        })
      );
      return;
    }

    const { count } = JSON.parse(current);
    const newCount = count + 1;

    if (newCount >= this.MAX_ATTEMPTS) {
      await this.logSecurityEvent({
        type: "ACCOUNT_LOCKED",
        identifier,
        timestamp: new Date(),
      });
    }

    await redis.setex(
      key,
      this.LOCKOUT_DURATION / 1000,
      JSON.stringify({
        count: newCount,
        lockUntil: Date.now() + this.LOCKOUT_DURATION,
      })
    );
  }

  static async resetAttempts(identifier: string): Promise<void> {
    await redis.del(`auth:attempts:${identifier}`);
  }

  static async logSecurityEvent(event: {
    type: string;
    identifier: string;
    timestamp: Date;
    userAgent?: string;
    ip?: string;
  }) {
    await db.securityEvents.create(event);
  }
}

Database Schema

-- Users table
CREATE TABLE users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  password_hash TEXT,
  role TEXT NOT NULL DEFAULT 'user',
  mfa_enabled BOOLEAN DEFAULT false,
  mfa_secret TEXT,
  mfa_enabled_at TIMESTAMP,
  active BOOLEAN DEFAULT true,
  created_at TIMESTAMP DEFAULT NOW()
);

-- WebAuthn credentials
CREATE TABLE webauthn_credentials (
  id SERIAL PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  credential_id BYTEA UNIQUE NOT NULL,
  credential_public_key BYTEA NOT NULL,
  counter BIGINT NOT NULL,
  transports TEXT[],
  created_at TIMESTAMP DEFAULT NOW()
);

-- Security events
CREATE TABLE security_events (
  id SERIAL PRIMARY KEY,
  type TEXT NOT NULL,
  identifier TEXT NOT NULL,
  timestamp TIMESTAMP NOT NULL,
  user_agent TEXT,
  ip TEXT,
  metadata JSON
);

Best Practices

Password Security

import bcrypt from "bcryptjs";

// Hash password
const hashPassword = async (password: string) => {
  return await bcrypt.hash(password, 12);
};

// Verify password
const verifyPassword = async (password: string, hash: string) => {
  return await bcrypt.compare(password, hash);
};

JWT Security

import jwt from "jsonwebtoken";

const generateToken = (payload: any) => {
  return jwt.sign(payload, process.env.JWT_SECRET!, {
    expiresIn: "7d",
    algorithm: "HS256",
  });
};

const verifyToken = (token: string) => {
  return jwt.verify(token, process.env.JWT_SECRET!);
};

Related Skills

  • moai-security-devsecops: Security automation
  • moai-security-encryption: Data encryption
  • moai-security-ssrf: SSRF prevention

Validation Checklist

Authentication:

  • NextAuth.js configured
  • OAuth providers set up
  • Credentials provider implemented
  • Session management configured

MFA:

  • TOTP implementation complete
  • QR code generation working
  • MFA verification tested
  • Backup codes generated

WebAuthn:

  • Passkey registration working
  • Passkey authentication working
  • Credential storage configured
  • Challenge management implemented

Security:

  • Rate limiting active
  • Account lockout configured
  • Security events logged
  • Password hashing secure

Last Updated: 2025-11-20