Password Provider
The Password Provider enables traditional username/password authentication with mandatory email verification via 6-digit magic codes. Users register with email and password, receive a verification code through a user-defined delivery callback (typically email), and must verify the code to complete authentication.
Features
- ✅ Email/Password Authentication - Traditional registration and login
- ✅ Magic Code Verification - 6-digit codes with configurable TTL (default 10 minutes)
- ✅ Rate Limiting - Max verification attempts (default 5) with atomic operations
- ✅ Password Policy - Configurable strength requirements (length, uppercase, lowercase, numbers, special chars)
- ✅ Password Reset Flow - Secure password reset with session tokens
- ✅ Password Change - Authenticated endpoint for password updates with session preservation
- ✅ Account Linking - Automatically links to existing OAuth accounts by email
- ✅ Custom Claims - Add custom JWT claims (static or callback)
- ✅ BCrypt Hashing - Configurable hash rounds (default 12)
Configuration
Configure the password provider in your nuxt.config.ts:
export default defineNuxtConfig({
nuxtAegis: {
providers: {
password: {
// Magic code settings
magicCodeTTL: 600, // 10 minutes (in seconds)
magicCodeMaxAttempts: 5, // Maximum verification attempts
// Password hashing
passwordHashRounds: 12, // BCrypt rounds (higher = more secure but slower)
// Password strength policy
passwordPolicy: {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumber: true,
requireSpecial: false,
},
},
},
},
})Then implement the required handlers in your server plugin:
// server/plugins/aegis.ts
export default defineNitroPlugin(() => {
defineAegisHandler({
// Unified database persistence for all auth methods
onUserPersist: async (user, { provider }) => {
const email = user.email as string
if (provider === 'password') {
const hashedPassword = user.hashedPassword as string
const dbUser = await database.upsertUser({
where: { email },
update: { hashedPassword },
create: { email, hashedPassword, role: 'user' },
})
return {
userId: dbUser.id,
role: dbUser.role,
permissions: dbUser.permissions,
}
}
// Handle OAuth providers...
// (see handlers guide for full example)
},
password: {
// Find user by email
async findUser(email) {
const user = await database.findUserByEmail(email)
if (!user || !user.hashedPassword) return null
return {
id: user.id,
email: user.email,
hashedPassword: user.hashedPassword,
name: user.name,
role: user.role,
}
},
// Send verification code
async sendVerificationCode(email, code, action) {
await sendEmail({
to: email,
subject: `Your ${action} verification code`,
text: `Your code is: ${code}`,
})
},
},
})
})Required Callbacks
findUser
Called to retrieve user data by email. Must return user with hashedPassword field or null if user doesn't exist.
Parameters:
email: string- The user's email address (normalized to lowercase)
Returns:
PasswordUser | null- User object withhashedPasswordornull
Example:
async findUser(email) {
const user = await db.users.findOne({ email })
if (!user || !user.hashedPassword) {
return null
}
return {
id: user.id,
email: user.email,
hashedPassword: user.hashedPassword,
name: user.name,
role: user.role,
permissions: user.permissions,
}
}Database Persistence
User persistence (creating/updating user records) is handled by the global onUserPersist handler, not in the password provider callbacks. This allows you to unify database logic across OAuth and password authentication. See the Handlers Guide for details.
sendVerificationCode
Called when a verification code is generated. Use this to send the code to the user via email with a clickable link.
Parameters:
email: string- The user's email address (normalized to lowercase)code: string- The 6-digit verification codeaction: 'register' | 'login' | 'reset'- The verification type
Important: Users should receive a clickable verification link in their email. The endpoints are GET requests that can be accessed via simple links:
- Registration:
/auth/password/register-verify?code={code} - Login:
/auth/password/login-verify?code={code} - Reset:
/auth/password/reset-verify?code={code}
Example with SendGrid:
import sgMail from '@sendgrid/mail'
async sendVerificationCode(email, code, action) {
const templates = {
register: 'Complete Your Registration',
login: 'Complete Your Login',
reset: 'Reset Your Password',
}
const endpoints = {
register: 'register-verify',
login: 'login-verify',
reset: 'reset-verify',
}
const baseUrl = process.env.BASE_URL || 'https://yourdomain.com'
const verifyLink = `${baseUrl}/auth/password/${endpoints[action]}?code=${code}`
await sgMail.send({
to: email,
from: 'noreply@yourdomain.com',
subject: templates[action],
html: `
<h1>Verification Code</h1>
<p>Your verification code is: <strong style="font-size: 24px; letter-spacing: 4px;">${code}</strong></p>
<p>Or click the button below to verify automatically:</p>
<a href="${verifyLink}" style="display: inline-block; padding: 12px 24px; background: #2196F3; color: white; text-decoration: none; border-radius: 4px;">
Verify ${action === 'register' ? 'Registration' : action === 'login' ? 'Login' : 'Reset'}
</a>
<p>This code expires in 10 minutes.</p>
<p><strong>Do not share this code with anyone.</strong></p>
`,
})
}Authentication Flows
Registration Flow
User submits registration (
POST /auth/password/register)- Email and password are validated
- Password strength is checked against policy
- User existence is checked via
findUser - Password is hashed with bcrypt
- 6-digit code is generated
sendVerificationCodeis called- Code is stored with email and hashed password
User verifies code (
POST /auth/password/register-verify)- Code is validated (max attempts, expiration)
onUserPersisthandler is called to create/update user- Custom claims are resolved
- Aegis CODE is generated
- User is redirected to
/auth/callback?code={aegis_code}
Login Flow
User submits login (
POST /auth/password/login)- Email is validated
- User is retrieved via
findUser - Password is verified with bcrypt
- 6-digit code is generated
sendVerificationCodeis called- Code is stored
User verifies code (
POST /auth/password/login-verify)- Code is validated (max attempts, expiration)
- Fresh user data is retrieved via
findUser - Custom claims are resolved
- Aegis CODE is generated
- User is redirected to
/auth/callback?code={aegis_code}
Password Reset Flow
User requests reset (
POST /auth/password/reset-request)- Email is validated
- User existence is checked via
findUser - Returns success regardless (prevents enumeration)
- If user exists, code is generated and
sendVerificationCodeis called
User verifies code (
POST /auth/password/reset-verify)- Code is validated
- Reset session is created (5-minute TTL)
- Returns
sessionId
User sets new password (
POST /auth/password/reset-complete)- Session is validated and consumed
- Password strength is checked
- Password is hashed
onUserPersisthandler is called to update password- All refresh tokens are invalidated
Password Change Flow
- Authenticated user changes password (
POST /auth/password/change)- User authentication is verified
- Current password is verified via
findUserand bcrypt - New password strength is checked
- New password is hashed
onUserPersisthandler is called to update password- All refresh tokens except current session are invalidated
- User stays logged in
API Endpoints
All endpoints return JSON responses with appropriate HTTP status codes.
POST /auth/password/register
Body:
{
"email": "user@example.com",
"password": "SecurePass123"
}Response:
{
"success": true
}Errors:
400- Invalid email format400- Password doesn't meet policy requirements (with specific errors)400- User already exists500- Failed to send verification code
GET /auth/password/register-verify
Query Parameters:
code=123456Response:
302 Redirect to /auth/callback?code={aegis_code}Errors:
400- Invalid or expired code400- Maximum verification attempts exceeded
POST /auth/password/login
Body:
{
"email": "user@example.com",
"password": "SecurePass123"
}Response:
{
"success": true
}Errors:
400- Invalid email or password (generic to prevent enumeration)500- Failed to send verification code
GET /auth/password/login-verify
Query Parameters:
code=123456Response:
302 Redirect to /auth/callback?code={aegis_code}Errors:
400- Invalid or expired code400- Maximum verification attempts exceeded
POST /auth/password/reset-request
Body:
{
"email": "user@example.com"
}Response:
{
"success": true,
"message": "If an account exists, a reset code has been sent"
}Always returns success to prevent user enumeration.
GET /auth/password/reset-verify
Query Parameters:
code=123456Response:
302 Redirect to /reset-password?session={sessionId}Note: You should create a password reset page at /reset-password that accepts the session query parameter and allows the user to enter a new password.
Errors:
400- Invalid or expired code400- Maximum verification attempts exceeded
POST /auth/password/reset-complete
Body:
{
"sessionId": "base64-session-token",
"newPassword": "NewSecurePass123"
}Response:
{
"success": true
}Errors:
400- Invalid or expired reset session400- Password doesn't meet policy requirements
POST /auth/password/change
Requires authentication (Bearer token)
Body:
{
"currentPassword": "SecurePass123",
"newPassword": "NewSecurePass123"
}Response:
{
"success": true
}Errors:
401- Not authenticated400- Current password is incorrect400- Password doesn't meet policy requirements
Security Considerations
Password Storage
Passwords are hashed using bcrypt with configurable rounds (default 12). Never store plaintext passwords. The hashedPassword field should only contain bcrypt hashes.
Email Verification
All authentication flows require email verification via magic codes. This prevents:
- Automated account creation
- Email enumeration attacks (reset flow)
- Unauthorized access
Rate Limiting
Magic codes have a maximum attempt limit (default 5) with atomic operations to prevent brute force attacks.
Session Management
- Reset sessions expire after 5 minutes
- Password changes preserve the current session but invalidate all others
- Password resets invalidate all sessions
Account Linking
Users are identified by email (normalized to lowercase). If a user registers with password authentication using the same email as an existing OAuth account, they can be automatically linked using the nuxt-aegis:success hook.
Custom Claims
Add custom data to JWT tokens using the customClaims option:
// Callback (dynamic)
customClaims: async (user) => {
const subscription = await getSubscription(user.id)
return {
role: user.role,
permissions: user.permissions,
subscription: subscription.tier,
}
}
// Static
customClaims: {
provider: 'password',
type: 'verified',
}Claims are available in the JWT token and can be accessed via useAuth().user.
TypeScript Types
import type { PasswordProviderConfig, PasswordUser } from '#nuxt-aegis'
// User type for onUserLookup return value
interface PasswordUser {
id?: string
email: string
hashedPassword: string
[key: string]: unknown // Additional fields for custom claims
}
// Provider configuration
interface PasswordProviderConfig {
magicCodeTTL?: number // Default: 600 (10 minutes)
magicCodeMaxAttempts?: number // Default: 5
passwordHashRounds?: number // Default: 12
passwordPolicy?: {
minLength?: number // Default: 8
requireUppercase?: boolean // Default: true
requireLowercase?: boolean // Default: true
requireNumber?: boolean // Default: true
requireSpecial?: boolean // Default: false
}
onMagicCodeGenerated: (
email: string,
code: string,
type: 'register' | 'login' | 'reset'
) => Promise<void>
onUserLookup: (email: string) => Promise<PasswordUser | null>
onUserPersist: (email: string, hashedPassword: string) => Promise<void>
customClaims?: CustomClaims<PasswordUser>
}Example Implementation
See the Password Authentication Guide for a complete implementation example with database integration.
Playground Example
Check out the playground for a working example with mock email delivery (console logging).