Auth
The auth module (@strav/auth) provides unopinionated, composable authentication primitives for building secure authentication systems. Unlike opinionated authentication frameworks, this package gives you the building blocks to implement any authentication pattern you need.
Installation
bun add @strav/auth
Requires @strav/kernel as a peer dependency for encryption utilities.
Architecture
The auth package is organized into five main areas:
1. JWT Operations
Zero-dependency JWT implementation using built-in Node.js/Bun crypto APIs, providing secure and standards-compliant token operations.
2. Token Management
Various token types for different authentication scenarios - signed opaque tokens, magic links, and refresh tokens.
3. TOTP / Two-Factor Authentication
Complete TOTP implementation following RFC 6238, including QR code generation and recovery codes.
4. Password Validation
Comprehensive password strength checking and policy enforcement utilities.
5. OAuth Helpers
State management for OAuth flows with CSRF protection.
JWT Operations
Basic JWT Operations
import { signJWT, verifyJWT } from '@strav/auth'
// Sign a JWT with custom claims
const token = await signJWT(
{
userId: 123,
role: 'admin',
permissions: ['read', 'write']
},
'your-secret-key',
{
expiresIn: '1h',
issuer: 'my-app',
audience: 'api.example.com'
}
)
// Verify and extract payload
const payload = await verifyJWT(token, 'your-secret-key', {
issuer: 'my-app',
audience: 'api.example.com',
requiredClaims: ['userId', 'role']
})
Access and Refresh Tokens
import { createAccessToken, verifyAccessToken, createRefreshToken, verifyRefreshToken } from '@strav/auth'
// Create token pair
const accessToken = await createAccessToken(
userId,
secret,
{ email: 'user@example.com', role: 'admin' }, // Additional claims
{ expiresIn: '15m' }
)
const refreshToken = await createRefreshToken(
userId,
secret,
{ expiresIn: '30d' }
)
// Verify tokens
const userId = await verifyAccessToken(accessToken, secret)
const refreshUserId = await verifyRefreshToken(refreshToken, secret)
Decode Without Verification
import { decodeJWT } from '@strav/auth'
// Decode for inspection (DO NOT use for authentication!)
const payload = decodeJWT(token)
console.log('Token expires at:', new Date(payload.exp! * 1000))
TOTP / Two-Factor Authentication
Generate TOTP Secret
import { generateSecret, totpUri } from '@strav/auth'
// Generate a new secret for a user
const { raw, base32 } = generateSecret()
// Save raw to database (encrypted)
await saveUserSecret(userId, raw)
// Generate QR code URI for authenticator apps
const uri = totpUri({
secret: base32,
issuer: 'MyApp',
account: user.email,
digits: 6,
period: 30
})
// Show QR code to user using the URI
Verify TOTP Codes
import { verifyTotp } from '@strav/auth'
// Get user's secret from database
const secret = await getUserSecret(userId)
// Verify the 6-digit code from user
const isValid = await verifyTotp(
secret,
userInputCode,
{
window: 1, // Allow 1 time step before/after for clock drift
digits: 6,
period: 30
}
)
if (!isValid) {
throw new Error('Invalid 2FA code')
}
Recovery Codes
import { generateRecoveryCodes } from '@strav/auth'
// Generate single-use recovery codes
const codes = generateRecoveryCodes(8)
// Hash and store these securely
for (const code of codes) {
await storeRecoveryCode(userId, await encrypt.hash(code))
}
// Show codes to user ONCE
return { recoveryCodes: codes }
Password Validation
Validate Against Policy
import { validatePassword, type PasswordPolicy } from '@strav/auth'
const policy: PasswordPolicy = {
minLength: 12,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
blacklist: ['password', 'company', '12345'],
customValidator: (pwd) => {
// Custom business logic
if (pwd.includes(username)) {
return { valid: false, message: 'Password cannot contain username' }
}
return { valid: true }
}
}
const result = validatePassword(userInput, policy)
if (!result.valid) {
// Show errors to user
return { errors: result.errors }
}
Calculate Password Strength
import { calculatePasswordStrength } from '@strav/auth'
const strength = calculatePasswordStrength(password)
// strength.score: 0-4 (Very Weak to Very Strong)
// strength.label: Human-readable label
// strength.issues: Specific problems found
// strength.suggestions: How to improve
if (strength.score < 3) {
return {
message: `Password is ${strength.label}`,
suggestions: strength.suggestions
}
}
Generate Secure Passwords
import { generatePassword } from '@strav/auth'
// Generate a 16-character password with all character types
const password = generatePassword(16)
// Generate without symbols (for compatibility)
const simplePassword = generatePassword(20, {
uppercase: true,
lowercase: true,
numbers: true,
symbols: false
})
Token Management
Signed Opaque Tokens
These tokens are encrypted and tamper-proof, perfect for email verification, password reset, etc.
import { createSignedToken, verifySignedToken } from '@strav/auth'
// Create a password reset token
const token = createSignedToken(
{
sub: userId,
typ: 'password-reset',
email: user.email
},
60 // Expires in 60 minutes
)
// Send token in email
await sendEmail(user.email, `Reset your password: ${url}?token=${token}`)
// Later, verify the token
try {
const payload = verifySignedToken(token)
if (payload.typ !== 'password-reset') {
throw new Error('Invalid token type')
}
// Proceed with password reset for payload.sub
} catch (error) {
// Token is invalid, expired, or tampered
}
Magic Link Tokens
Specialized tokens for passwordless authentication.
import { createMagicLinkToken, verifyMagicLinkToken } from '@strav/auth'
// Create magic link
const token = createMagicLinkToken(userId, {
email: user.email,
redirect: '/dashboard',
expiresInMinutes: 15
})
const magicLink = `https://app.com/auth/magic?token=${token}`
await sendEmail(user.email, `Click to login: ${magicLink}`)
// Verify when user clicks link
try {
const payload = verifyMagicLinkToken(token)
// Log user in as payload.sub
// Redirect to payload.redirect if provided
} catch (error) {
// Token invalid or expired
}
Refresh Token Management
Build a token rotation strategy with storage.
import { createTokenRotation, generateRefreshToken } from '@strav/auth'
// Create rotation strategy with your storage backend
const rotation = createTokenRotation({
async store(userId, token, expiresAt) {
await db.refreshTokens.create({
userId,
token,
expiresAt,
deviceId: getDeviceId()
})
},
async verify(token) {
const record = await db.refreshTokens.findByToken(token)
if (!record || record.expiresAt < new Date()) {
return null
}
return record.userId
},
async revoke(token) {
await db.refreshTokens.deleteByToken(token)
},
async revokeAll(userId) {
await db.refreshTokens.deleteByUserId(userId)
}
})
// Generate new refresh token
const refreshToken = await rotation.generate(userId, 2592000) // 30 days
// Verify and rotate
const userId = await rotation.verify(refreshToken)
if (userId) {
// Revoke old token
await rotation.revoke(refreshToken)
// Issue new token pair
const newRefresh = await rotation.generate(userId)
const newAccess = await createAccessToken(userId, secret)
}
OAuth State Management
Protect against CSRF attacks in OAuth flows.
import { createOAuthStateStore } from '@strav/auth'
// Create state store (use Redis/DB in production)
const stateStore = createOAuthStateStore({
async store(state) {
await redis.setex(`oauth:${state.value}`, 600, JSON.stringify(state))
},
async retrieve(value) {
const data = await redis.get(`oauth:${value}`)
return data ? JSON.parse(data) : null
},
async delete(value) {
await redis.del(`oauth:${value}`)
},
ttl: 600 // 10 minutes
})
// Before redirecting to OAuth provider
const stateValue = await stateStore.generate({
redirect: '/dashboard',
data: { provider: 'github' }
})
const authUrl = `https://github.com/login/oauth/authorize?state=${stateValue}&...`
// After OAuth callback
const state = await stateStore.verify(req.query.state)
if (!state) {
throw new Error('Invalid or expired state')
}
// Continue with OAuth flow
// Use state.redirect and state.data as needed
Integration Examples
Complete Login Flow
import { verifyAccessToken, createAccessToken, createRefreshToken } from '@strav/auth'
import { encrypt } from '@strav/kernel'
async function login(email: string, password: string) {
// Find user
const user = await User.findByEmail(email)
if (!user) {
throw new Error('Invalid credentials')
}
// Verify password
const valid = await encrypt.verify(password, user.passwordHash)
if (!valid) {
throw new Error('Invalid credentials')
}
// Create tokens
const accessToken = await createAccessToken(user.id, JWT_SECRET)
const refreshToken = await createRefreshToken(user.id, JWT_SECRET)
// Store refresh token
await storeRefreshToken(user.id, refreshToken)
return { accessToken, refreshToken }
}
Protected Route Middleware
import { verifyAccessToken } from '@strav/auth'
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'No token provided' })
}
try {
const userId = await verifyAccessToken(token, JWT_SECRET)
req.user = await User.findById(userId)
next()
} catch (error) {
return res.status(401).json({ error: 'Invalid token' })
}
}
Password Reset Flow
import { createSignedToken, verifySignedToken, validatePassword } from '@strav/auth'
async function requestPasswordReset(email: string) {
const user = await User.findByEmail(email)
if (!user) {
// Don't reveal if email exists
return { message: 'If an account exists, you will receive an email' }
}
const token = createSignedToken(
{ sub: user.id, typ: 'password-reset' },
60 // 1 hour
)
await sendResetEmail(email, token)
}
async function resetPassword(token: string, newPassword: string) {
// Verify token
const payload = verifySignedToken(token)
if (payload.typ !== 'password-reset') {
throw new Error('Invalid token')
}
// Validate new password
const validation = validatePassword(newPassword, passwordPolicy)
if (!validation.valid) {
return { errors: validation.errors }
}
// Update password
const user = await User.findById(payload.sub)
user.passwordHash = await encrypt.hash(newPassword)
await user.save()
// Invalidate all sessions
await revokeAllRefreshTokens(user.id)
}
Security Considerations
- Zero Dependencies: This package has no external dependencies, eliminating supply chain attack vectors that could compromise your authentication system
- Secret Management: Store JWT secrets securely using environment variables and rotate them periodically
- Token Storage: Never store tokens in localStorage for sensitive apps; use httpOnly cookies when possible
- Password Policies: Enforce strong password requirements appropriate for your security needs
- TOTP Secrets: Always encrypt TOTP secrets before storing in the database
- Recovery Codes: Hash recovery codes before storage and mark as used after verification
- Rate Limiting: Implement rate limiting on authentication endpoints
- Token Rotation: Implement refresh token rotation to limit exposure window