@goobits/security
v1.2.0
Published
Comprehensive security utilities for Goobits projects - CSRF, reCAPTCHA, validation, rate limiting, admin auth, and audit logging
Maintainers
Readme
@goobits/security
Version 1.2.0 - Stable
Comprehensive security utilities for Goobits projects including CSRF protection, reCAPTCHA verification, request validation, rate limiting, admin authentication, and audit logging. Built for SvelteKit applications with integrated logging via @goobits/logger.
🔒 Features
- 🛡️ CSRF Protection: Double-submit cookie pattern with timing-safe comparison and token expiration
- 🤖 reCAPTCHA Verification: Support for both v2 and v3 with score thresholds and flexible options API
- ✅ Request Validation: Zod-based middleware for body, query, and params
- ⏱️ Rate Limiting: In-memory and Redis-based rate limiting with factory pattern ⭐ NEW in v1.2.0
- 🔐 Admin Authentication: JWT-based admin auth with API key fallback ⭐ NEW in v1.2.0
- 📝 Audit Logging: Security-sensitive operation tracking for GDPR compliance ⭐ NEW in v1.2.0
- ⏰ Token Expiration: Automatic CSRF token expiration and cleanup (1-hour default)
- 📊 Integrated Logging: Built-in logging via @goobits/logger
- 🔧 SvelteKit Native: Designed for SvelteKit's request/response model
- 🧪 Test-Friendly: Debug flags for development and testing
- 📦 Minimal Dependencies: Only depends on @goobits/logger, jsonwebtoken, and peer deps
📦 Installation
# In workspace monorepo
pnpm add @goobits/security
# Or in individual package
npm install @goobits/security zod🛡️ CSRF Protection
Implements the double-submit cookie pattern for CSRF protection with timing-safe token comparison.
Quick Start
// src/routes/api/contact/+server.js
import { validateCsrfToken, createCsrfProtectedResponse } from '@goobits/security'
import { json } from '@sveltejs/kit'
export async function POST({ request }) {
// Verify CSRF token
if (!validateCsrfToken(request)) {
return json({ error: 'Invalid CSRF token' }, { status: 403 })
}
// Process the request...
const result = await processContactForm(data)
// Return response with new CSRF token
return createCsrfProtectedResponse(
json({ success: true, data: result })
)
}API Reference
generateCsrfToken()
Generate a new random CSRF token.
import { generateCsrfToken } from '@goobits/security'
const token = generateCsrfToken()
// Returns: 64-character hex stringsetCsrfCookie(response, token)
Set CSRF token as HTTP-only cookie in response.
import { setCsrfCookie } from '@goobits/security'
export async function GET() {
const response = json({ message: 'Hello' })
const token = generateCsrfToken()
setCsrfCookie(response, token)
return response
}getCsrfToken(request)
Extract CSRF token from request cookies.
import { getCsrfToken } from '@goobits/security'
const token = getCsrfToken(request)
// Returns: token string or nullvalidateCsrfToken(request)
Validate CSRF token using double-submit cookie pattern with timing-safe comparison.
import { validateCsrfToken } from '@goobits/security'
if (!validateCsrfToken(request)) {
return json({ error: 'Invalid CSRF token' }, { status: 403 })
}createCsrfProtectedResponse(response)
Convenience function to generate token and set cookie in one call.
import { createCsrfProtectedResponse } from '@goobits/security'
return createCsrfProtectedResponse(
json({ success: true })
)generateCsrfTokenWithExpiry(options) ⭐ NEW in v1.1.0
Generate a CSRF token with expiration tracking.
import { generateCsrfTokenWithExpiry } from '@goobits/security'
// Default: 1 hour expiry with tracking
const token = generateCsrfTokenWithExpiry()
// Custom expiry time
const token = generateCsrfTokenWithExpiry({ expiryMs: 30 * 60 * 1000 }) // 30 minutes
// Without expiry tracking
const token = generateCsrfTokenWithExpiry({ trackExpiry: false })validateCsrfToken(request, options) - Enhanced in v1.1.0
Now supports expiry checking with options parameter:
import { validateCsrfToken } from '@goobits/security'
// Basic validation (backward compatible)
if (!validateCsrfToken(request)) {
return json({ error: 'Invalid CSRF token' }, { status: 403 })
}
// With expiry checking
if (!validateCsrfToken(request, { checkExpiry: true })) {
return json({ error: 'Invalid or expired CSRF token' }, { status: 403 })
}Token Store Management ⭐ NEW in v1.1.0
Utilities for managing the in-memory token store:
import {
getCsrfTokenStoreSize,
cleanupCsrfTokens,
clearCsrfTokenStore
} from '@goobits/security'
// Get number of tracked tokens
console.log(`Active tokens: ${ getCsrfTokenStoreSize() }`)
// Manually cleanup expired tokens
const cleaned = cleanupCsrfTokens()
console.log(`Cleaned up ${ cleaned } expired tokens`)
// Clear all tokens (useful for testing)
clearCsrfTokenStore()Configuration
// Disable CSRF validation for testing (DO NOT USE IN PRODUCTION)
// Set environment variable: DISABLE_CSRF=trueCookie Settings:
- Name:
csrf-token - HttpOnly:
true - Secure:
true(production),false(development) - SameSite:
lax - Path:
/ - MaxAge: 24 hours
Header: X-CSRF-Token
Client-Side Integration
// Fetch the CSRF token from cookie and send in header
async function submitForm(data) {
const csrfToken = getCookie('csrf-token')
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
})
return response.json()
}
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
return match ? match[2] : null
}🤖 reCAPTCHA Verification
Server-side reCAPTCHA token verification supporting both v2 (checkbox) and v3 (score-based).
Setup
# Set environment variable
RECAPTCHA_SECRET_KEY=your_secret_key_hereBasic Usage
// src/routes/api/contact/+server.js
import { verifyRecaptchaToken } from '@goobits/security'
import { json } from '@sveltejs/kit'
export async function POST({ request }) {
const data = await request.json()
const { recaptchaToken } = data
// Verify token (v2 or v3)
const isValid = await verifyRecaptchaToken(recaptchaToken)
if (!isValid) {
return json({ error: 'reCAPTCHA verification failed' }, { status: 400 })
}
// Process the request...
return json({ success: true })
}reCAPTCHA v3 with Score and Action
import { verifyRecaptchaToken } from '@goobits/security'
// Positional arguments (backward compatible)
const isValid = await verifyRecaptchaToken(
token,
'contact_form', // Expected action
0.7 // Minimum score (0.0 - 1.0)
)
// Options object pattern ⭐ NEW in v1.1.0
const isValid = await verifyRecaptchaToken(token, {
action: 'contact_form',
minScore: 0.7,
allowInDevelopment: false
})
if (!isValid) {
return json({ error: 'reCAPTCHA failed' }, { status: 400 })
}Detailed Verification
For debugging and monitoring, use the detailed verification function:
import { verifyRecaptchaTokenWithDetails } from '@goobits/security'
const result = await verifyRecaptchaTokenWithDetails(token, 'submit', 0.5)
if (!result.success) {
console.error('Verification failed:', result.error, result.details)
return json({ error: 'Verification failed' }, { status: 400 })
}
// Access detailed information
console.log('Score:', result.score)
console.log('Action:', result.action)
console.log('Timestamp:', result.challenge_ts)API Reference
verifyRecaptchaToken(token, actionOrOptions, minScore) - Enhanced in v1.1.0
Now supports both positional arguments (backward compatible) and options object pattern.
Positional Arguments (backward compatible):
token(string, required): The reCAPTCHA response tokenaction(string, optional): Expected action name (v3 only)minScore(number, optional, default: 0.5): Minimum score threshold (v3 only, 0.0 to 1.0)
Options Object ⭐ NEW:
token(string, required): The reCAPTCHA response tokenoptions(object, optional):action(string): Expected action name (v3 only)minScore(number, default: 0.5): Minimum score threshold (0.0 to 1.0)secretKey(string): Override RECAPTCHA_SECRET_KEY env varallowInDevelopment(boolean, default: true): Bypass verification in dev when secret key is missing
Returns: Promise<boolean>
Examples:
// Positional args (backward compatible)
await verifyRecaptchaToken(token, 'submit_form', 0.7)
// Options object
await verifyRecaptchaToken(token, {
action: 'submit_form',
minScore: 0.7,
allowInDevelopment: false
})verifyRecaptchaTokenWithDetails(token, action, minScore)
Same parameters as above, but returns detailed result object:
{
success: boolean
score?: number // v3 only
action?: string // v3 only
challenge_ts?: string // ISO timestamp
hostname?: string // Request hostname
error?: string // Error message if failed
details?: object // Additional error details
devBypass?: boolean // True if bypassed in development
}Development Mode
In development (NODE_ENV !== 'production'), if RECAPTCHA_SECRET_KEY is not set:
verifyRecaptchaToken()returnstrue(bypasses verification)verifyRecaptchaTokenWithDetails()returns{ success: false, devBypass: true }
In production, missing secret key always fails verification.
✅ Request Validation
Zod-based middleware for validating request body, query parameters, and URL parameters.
Basic Usage
// src/routes/api/users/+server.js
import { withValidation } from '@goobits/security'
import { z } from 'zod'
import { json } from '@sveltejs/kit'
// Define schemas
const createUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().positive().optional()
})
// Wrap handler with validation
export const POST = withValidation({
body: createUserSchema
})(async (event) => {
const { body } = event.locals.validated
// body is now typed and validated
const user = await createUser(body)
return json({ success: true, user })
})Validating Query Parameters
import { withValidation } from '@goobits/security'
import { z } from 'zod'
const searchSchema = z.object({
q: z.string().min(1),
page: z.string().regex(/^\d+$/).transform(Number).optional(),
limit: z.string().regex(/^\d+$/).transform(Number).optional()
})
export const GET = withValidation({
query: searchSchema
})(async (event) => {
const { query } = event.locals.validated
// query.q is string, query.page and query.limit are numbers
const results = await search(query.q, query.page, query.limit)
return json({ results })
})Validating URL Parameters
// src/routes/api/users/[id]/+server.js
import { withValidation } from '@goobits/security'
import { z } from 'zod'
const paramsSchema = z.object({
id: z.string().uuid()
})
export const GET = withValidation({
params: paramsSchema
})(async (event) => {
const { params } = event.locals.validated
const user = await getUserById(params.id)
return json({ user })
})Combined Validation
const bodySchema = z.object({
name: z.string().min(2),
status: z.enum(['active', 'inactive'])
})
const querySchema = z.object({
notify: z.string().transform(val => val === 'true')
})
const paramsSchema = z.object({
id: z.string().uuid()
})
export const PUT = withValidation({
body: bodySchema,
query: querySchema,
params: paramsSchema
})(async (event) => {
const { body, query, params } = event.locals.validated
const user = await updateUser(params.id, body, query.notify)
return json({ user })
})Alternative Pattern: getInputValidator
For more control, use the validator directly:
import { getInputValidator } from '@goobits/security'
import { z } from 'zod'
const schema = z.object({
email: z.string().email()
})
export async function POST({ request }) {
const validator = getInputValidator(schema)
const body = await request.json()
const result = validator(body)
if (!result.success) {
return json({
error: 'Validation failed',
details: result.error
}, { status: 400 })
}
// Use result.data (validated and typed)
return json({ success: true, email: result.data.email })
}Error Responses
Validation failures return:
{
"success": false,
"error": "Invalid request data",
"details": [
{
"code": "too_small",
"minimum": 2,
"path": ["name"],
"message": "String must contain at least 2 character(s)"
}
]
}⏱️ Rate Limiting
⭐ NEW in v1.2.0 - Protect your API endpoints from abuse with in-memory or Redis-based rate limiting.
Quick Start
// src/routes/api/contact/+server.js
import { withRateLimit } from '@goobits/security'
import { json } from '@sveltejs/kit'
export const POST = withRateLimit({
action: 'contact-form',
windowMs: 60000, // 1 minute
maxRequests: 5, // 5 requests per minute
message: 'Too many requests. Please try again later.'
})(async ({ request, locals }) => {
// Your handler code here
return json({ success: true })
})Factory Pattern (In-Memory vs Redis)
The rate limiter automatically selects the appropriate backend:
import { getRateLimiter } from '@goobits/security'
// Get the configured rate limiter (in-memory or Redis)
const rateLimiter = await getRateLimiter()
// Use it directly
const result = await rateLimiter.rateLimitRequest('user-123', 'login')
if (!result.allowed) {
return json({ error: 'Rate limit exceeded' }, { status: 429 })
}Configuration:
- Development: Uses in-memory rate limiting by default
- Production: Uses Redis if
REDIS_HOSTorREDIS_URLis set - Force Redis in dev: Set
FORCE_REDIS_RATELIMIT=true
Middleware Patterns
Form Submission Rate Limiting
import { rateLimitFormSubmission } from '@goobits/security'
export const POST = async ({ request, locals }) => {
const data = await request.json()
// Rate limit by IP + email
const rateLimitResult = await rateLimitFormSubmission(
locals.clientIP,
data.email,
'contact',
{ maxAttempts: 3, windowMinutes: 10 }
)
if (!rateLimitResult.allowed) {
return json({
error: 'Too many attempts. Please try again later.',
retryAfter: rateLimitResult.retryAfter
}, { status: 429 })
}
// Process form...
return json({ success: true })
}Custom Rate Limit Handler
import { createRateLimitHandler } from '@goobits/security'
const apiRateLimit = createRateLimitHandler({
action: 'api-call',
windowMs: 60000,
maxRequests: 100,
keyGenerator: (event) => event.locals.userId || event.locals.clientIP
})
export const GET = async (event) => {
const rateLimitResponse = await apiRateLimit(event)
if (rateLimitResponse) {
return rateLimitResponse // 429 response
}
// Handle request...
return json({ data: [] })
}API Reference
withRateLimit(options)
Middleware wrapper for rate limiting.
Options:
action(string): Action identifierwindowMs(number): Time window in millisecondsmaxRequests(number): Max requests per windowmessage(string): Error messagekeyGenerator(function): Custom key function (optional)skipSuccessfulRequests(boolean): Don't count successful requests (default: false)
rateLimitRequest(identifier, action)
Check rate limit for an identifier.
Returns: { allowed: boolean, remaining: number, retryAfter?: number }
cleanupRateLimits()
Manually cleanup expired rate limit entries (automatic in background).
🔐 Admin Authentication
⭐ NEW in v1.2.0 - JWT-based admin authentication with audit logging integration.
Setup
# Required environment variables
JWT_SECRET=your_jwt_secret_here
# Optional: Admin API key for service-to-service auth
ADMIN_API_KEY=your_api_key_hereBasic Usage
// src/routes/api/admin/users/+server.js
import { requireAdmin } from '@goobits/security'
import { json } from '@sveltejs/kit'
export const GET = requireAdmin(async ({ locals }) => {
// locals.adminUser contains authenticated admin info
const { adminUser } = locals
console.log('Admin user:', adminUser.id, adminUser.role)
// Admin-only logic here
const users = await getAllUsers()
return json({ users })
})Authentication Methods
1. JWT Bearer Token
// Client request with JWT
fetch('/api/admin/users', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
}
})2. API Key
// Client request with API key (header)
fetch('/api/admin/users', {
headers: {
'Authorization': 'ApiKey your_api_key_here'
}
})
// Or using custom header
fetch('/api/admin/users', {
headers: {
'X-Admin-API-Key': 'your_api_key_here'
}
})API Reference
requireAdmin(handler)
Middleware that requires admin authentication. Returns 401 if not authenticated.
Features:
- Verifies JWT tokens with admin role
- Supports API key authentication
- Automatic audit logging
- Sets
locals.adminUserfor handlers
getAdminUser(request)
Extract and verify admin credentials from request.
Returns: Admin user object or null
{
id: string,
email?: string,
role: 'admin',
authMethod: 'jwt' | 'apikey'
}createAdminToken(payload)
Create a JWT token with admin role (for testing/development).
import { createAdminToken } from '@goobits/security'
const token = createAdminToken({
id: 'user-123',
email: '[email protected]'
})
// Expires in 24 hoursgenerateAdminApiKey()
Generate a secure random API key.
import { generateAdminApiKey } from '@goobits/security'
const apiKey = await generateAdminApiKey()
// Returns: 64-character hex stringJWT Token Requirements
Your JWT must include one of:
role: 'admin'isAdmin: trueadmin: true
// Example JWT payload
{
"id": "user-123",
"email": "[email protected]",
"role": "admin",
"iat": 1234567890,
"exp": 1234654290
}📝 Audit Logging
⭐ NEW in v1.2.0 - Track security-sensitive operations for compliance and monitoring.
Basic Usage
// src/routes/api/admin/delete-user/+server.js
import { withAuditLogging, auditLog } from '@goobits/security'
import { json } from '@sveltejs/kit'
export const DELETE = withAuditLogging({
action: 'delete_user',
includeRequestBody: true
})(async ({ request, locals, params }) => {
const userId = params.id
// Delete the user
await deleteUser(userId)
// Log specific audit event
await auditLog({
eventType: 'user.deleted',
severity: 'high',
actor: locals.adminUser?.email || 'system',
action: 'delete_user',
outcome: 'success',
metadata: {
userId,
deletedBy: locals.adminUser?.id
},
ipAddress: locals.clientIP,
userAgent: request.headers.get('user-agent')
})
return json({ success: true })
})Middleware Usage
import { withAuditLogging } from '@goobits/security'
// Automatically log request start/end with timing
export const POST = withAuditLogging({
action: 'payment_processed',
includeRequestBody: false, // Don't log sensitive payment data
includeResponse: false
})(async (event) => {
// Handler code...
return json({ success: true })
})Direct Logging
import { auditLog } from '@goobits/security'
await auditLog({
eventType: 'login.failed',
severity: 'medium',
actor: email,
action: 'user_login',
outcome: 'failure',
metadata: {
reason: 'invalid_password',
attempts: 3
},
ipAddress: request.headers.get('x-forwarded-for'),
userAgent: request.headers.get('user-agent')
})Audit Event Structure
{
eventType: string // Event classification (e.g., 'user.deleted')
severity: string // 'info' | 'low' | 'medium' | 'high' | 'critical'
actor: string // Who performed the action
action: string // What action was performed
outcome: string // 'success' | 'failure' | 'blocked'
metadata?: object // Additional context
ipAddress?: string // Client IP
userAgent?: string // User agent
timestamp: string // ISO 8601 timestamp (auto-added)
}Integration with Admin Auth
Admin authentication automatically logs:
- Successful admin logins
- Failed authentication attempts
- Admin access to protected endpoints
// Combines admin auth + audit logging
import { requireAdmin } from '@goobits/security'
export const DELETE = requireAdmin(async ({ locals }) => {
// Admin auth automatically logs:
// - admin.authentication_success on successful auth
// - admin.authentication_failed on failed auth
// Your admin logic here...
return json({ success: true })
})GDPR Compliance
Audit logs via @goobits/logger include:
- Timestamp for all events
- Actor identification
- Action performed
- Outcome tracking
- IP address logging (for security)
Configure retention policies in your logging infrastructure.
🔐 Security Best Practices
1. CSRF Protection
DO:
- ✅ Use CSRF tokens for all state-changing operations (POST, PUT, DELETE)
- ✅ Validate tokens on the server side
- ✅ Rotate tokens after sensitive operations
- ✅ Use secure, HTTP-only cookies
DON'T:
- ❌ Disable CSRF in production
- ❌ Store CSRF tokens in localStorage (XSS vulnerability)
- ❌ Skip validation for "internal" APIs
- ❌ Use predictable token generation
2. reCAPTCHA
DO:
- ✅ Set appropriate score thresholds for your use case (0.5 is a good default)
- ✅ Validate action names to prevent token reuse
- ✅ Keep your secret key secure (environment variables)
- ✅ Monitor scores and adjust thresholds over time
DON'T:
- ❌ Trust client-side validation alone
- ❌ Use the same reCAPTCHA for different actions
- ❌ Set score threshold too high (may block legitimate users)
- ❌ Commit secret keys to version control
3. Request Validation
DO:
- ✅ Validate all user input (body, query, params)
- ✅ Use strict schemas with appropriate constraints
- ✅ Sanitize output when rendering user data
- ✅ Log validation failures for monitoring
DON'T:
- ❌ Trust any client-side validation
- ❌ Skip validation for "internal" endpoints
- ❌ Expose detailed error messages in production
- ❌ Use overly permissive schemas
4. General
DO:
- ✅ Use HTTPS in production
- ✅ Set appropriate CORS policies
- ✅ Implement rate limiting
- ✅ Keep dependencies updated
- ✅ Use environment-based configuration
DON'T:
- ❌ Disable security features in production
- ❌ Log sensitive data (passwords, tokens)
- ❌ Trust X-Forwarded-For without validation
- ❌ Use default or weak secrets
🏗️ SvelteKit Integration
Complete Example: Contact Form
// src/routes/api/contact/+server.js
import {
validateCsrfToken,
verifyRecaptchaToken,
withValidation,
createCsrfProtectedResponse
} from '@goobits/security'
import { z } from 'zod'
import { json } from '@sveltejs/kit'
const contactSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
message: z.string().min(10).max(5000),
recaptchaToken: z.string()
})
export const POST = withValidation({
body: contactSchema
})(async (event) => {
const { request, locals } = event
const { body } = locals.validated
// 1. Validate CSRF token
if (!validateCsrfToken(request)) {
return json({ error: 'Invalid CSRF token' }, { status: 403 })
}
// 2. Verify reCAPTCHA
const isHuman = await verifyRecaptchaToken(
body.recaptchaToken,
'contact_form',
0.5
)
if (!isHuman) {
return json({ error: 'reCAPTCHA verification failed' }, { status: 400 })
}
// 3. Process the contact form
await sendEmail({
to: '[email protected]',
from: body.email,
subject: `Contact from ${body.name}`,
text: body.message
})
// 4. Return success with new CSRF token
return createCsrfProtectedResponse(
json({ success: true, message: 'Message sent successfully' })
)
})Hooks Integration
// src/hooks.server.js
import { generateCsrfToken, setCsrfCookie } from '@goobits/security'
export async function handle({ event, resolve }) {
// Generate CSRF token for GET requests if not present
if (event.request.method === 'GET') {
const response = await resolve(event)
// Add CSRF token to response if it's an HTML page
if (response.headers.get('content-type')?.includes('text/html')) {
const token = generateCsrfToken()
setCsrfCookie(response, token)
}
return response
}
return resolve(event)
}📊 Logging
All security operations are logged via @goobits/logger:
import { LoggerConfig, LogLevel } from '@goobits/logger'
// Configure log level for security operations
LoggerConfig.setLogLevel(LogLevel.INFO)
// Security logs will show with [CSRF], [reCAPTCHA], or [Validation] prefixesLogged Events:
- CSRF token generation and validation
- reCAPTCHA verification attempts and failures
- Request validation errors
- Security configuration warnings
🧪 Testing
Disable CSRF for Tests
# In test environment
DISABLE_CSRF=trueOr programmatically:
import { DISABLE_CSRF } from '@goobits/security'
if (DISABLE_CSRF) {
console.log('CSRF validation is disabled')
}Mock reCAPTCHA
// In development, set invalid secret to bypass
// (verifyRecaptchaToken returns true when secret is missing in dev mode)
delete process.env.RECAPTCHA_SECRET_KEY📄 License
MIT
🤝 Contributing
Part of the Goobits monorepo. See main repository for contribution guidelines.
🔗 Related Packages
- @goobits/logger - Unified logging (used internally)
- @goobits/store - E-commerce components using security utilities
- @goobits/ui - UI components
