from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from './database';
export const identityProvider = betterAuth({
database: drizzleAdapter(db, { provider: 'sqlite' }),
user: {
additionalFields: {
tenantId: { type: 'string', required: true },
},
},
password: {
hash: 'argon2',
config: {
memoryCost: 19456,
timeCost: 2,
parallelism: 1,
},
},
session: {
expiresIn: 604800, // 7 days
updateAge: 86400, // Refresh daily
cookieCache: {
enabled: true,
maxAge: 300, // Cache session in cookie for 5 mins to reduce DB load
},
},
advanced: {
cookieAttributes: {
secure: true,
sameSite: 'strict',
httpOnly: true,
},
},
});
**Rationale:**
- **Argon2id:** Provides the highest security margin for password hashing.
- **Cookie Caching:** Reduces database queries for session validation by caching session data in the cookie itself for a short duration, balancing performance and security.
- **Strict Cookie Attributes:** `HttpOnly` prevents JavaScript access, `Secure` ensures transmission over HTTPS, and `SameSite=Strict` mitigates CSRF risks.
#### 2. Granular Access Control with Organization Scoping
Authorization must enforce the principle of least privilege and ensure strict data isolation between tenants. Role-Based Access Control (RBAC) combined with organization scoping prevents Insecure Direct Object Reference (IDOR) vulnerabilities.
**Implementation:**
Create an access control engine that evaluates permissions and enforces tenant boundaries.
```typescript
// src/security/access-control.ts
export class AccessControlEngine {
private policies: Map<string, string[]>;
constructor() {
this.policies = new Map([
['super_admin', ['*:*']],
['tenant_admin', ['billing:manage', 'users:invite', 'settings:update']],
['member', ['billing:read', 'users:read', 'settings:read']],
]);
}
public authorize(
principal: { role: string; tenantId: string },
resource: string,
action: string
): boolean {
const permissions = this.policies.get(principal.role) || [];
const hasPermission = permissions.some(
(p) => p === `${resource}:${action}` || p === `${resource}:*` || p === '*:*'
);
if (!hasPermission) {
throw new Error('Access denied: insufficient permissions');
}
// Tenant scoping is enforced at the query level, but the engine
// ensures the principal has a valid tenant context.
if (!principal.tenantId) {
throw new Error('Access denied: missing tenant context');
}
return true;
}
}
export const acl = new AccessControlEngine();
Rationale:
- Centralized Policy: Encapsulates permission logic, making it easier to audit and modify.
- Tenant Context: Requires
tenantId in the principal object, ensuring that all authorization checks are aware of the multi-tenant boundary.
- Wildcard Support: Allows flexible permission definitions (e.g.,
billing:* grants all billing actions).
3. Application-Level Data Encryption
While Cloudflare D1 provides encryption at rest, sensitive fields like PII should be encrypted at the application level to protect against database dumps and insider threats. AES-GCM is used for authenticated encryption.
Implementation:
Develop a utility class for encrypting and decrypting sensitive data before persistence.
// src/security/data-vault.ts
export class DataVault {
private key: CryptoKey;
constructor(keyMaterial: Uint8Array) {
this.key = crypto.subtle.importKey(
'raw',
keyMaterial,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt']
);
}
public async seal(plaintext: string): Promise<{ cipher: string; nonce: string }> {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plaintext);
const cipher = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: nonce },
await this.key,
encoded
);
return {
cipher: btoa(String.fromCharCode(...new Uint8Array(cipher))),
nonce: btoa(String.fromCharCode(...nonce)),
};
}
public async open(cipher: string, nonce: string): Promise<string> {
const decoded = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: Uint8Array.from(atob(nonce), (c) => c.charCodeAt(0)) },
await this.key,
Uint8Array.from(atob(cipher), (c) => c.charCodeAt(0))
);
return new TextDecoder().decode(decoded);
}
}
Rationale:
- Defense in Depth: Adds a layer of protection even if the database is compromised.
- AES-GCM: Provides both confidentiality and integrity, ensuring data has not been tampered with.
- Nonce Management: Generates a unique nonce for each encryption operation, which is critical for security.
4. API Resilience and Validation
APIs must be protected against abuse and malformed input. Rate limiting prevents brute-force attacks, while schema validation ensures data integrity.
Implementation:
Implement a sliding window rate limiter and use Zod for strict input validation.
// src/security/rate-limiter.ts
const buckets = new Map<string, number[]>();
export function enforceRateLimit(ip: string, limit: number, windowMs: number): boolean {
const now = Date.now();
const timestamps = buckets.get(ip) || [];
const valid = timestamps.filter((t) => now - t < windowMs);
if (valid.length >= limit) {
return false;
}
valid.push(now);
buckets.set(ip, valid);
return true;
}
// src/validation/schemas.ts
import { z } from 'zod';
export const userRegistrationSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(12).regex(/[A-Z]/, 'Must contain uppercase').regex(/[0-9]/, 'Must contain number'),
tenantName: z.string().min(3).max(50),
});
Rationale:
- Sliding Window: More accurate than fixed windows, preventing burst attacks at window boundaries.
- Zod Validation: Provides runtime type safety and ensures that only valid data enters the business logic, preventing injection attacks and data corruption.
Pitfall Guide
| Pitfall | Explanation | Fix |
|---|
| LocalStorage JWTs | Storing tokens in localStorage exposes them to XSS attacks. Malicious scripts can read and exfiltrate tokens. | Use HttpOnly, Secure cookies for session management. Never store sensitive tokens in client-accessible storage. |
| Hardcoded RBAC | Embedding roles and permissions directly in code makes it difficult to update policies and audit access. | Store permissions in a database or configuration file. Use a centralized policy engine to evaluate access. |
| Missing Tenant Scoping | Failing to filter queries by tenantId allows users to access data from other organizations (IDOR). | Always include tenantId in query WHERE clauses. Enforce tenant context in the authorization layer. |
| Weak Password Hashing | Using bcrypt or scrypt is acceptable, but Argon2id is superior against GPU-based attacks. | Migrate to Argon2id with OWASP-recommended parameters. |
| CSP Misconfiguration | Using unsafe-inline in Content-Security-Policy headers defeats the purpose of CSP. | Use nonces or hashes for inline scripts. Strictly define allowed sources. |
| TOCTOU in AuthZ | Time-of-Check to Time-of-Use vulnerabilities occur when permissions change between validation and execution. | Use short-lived tokens or atomic checks. Re-validate permissions immediately before critical operations. |
| Key Management Failures | Hardcoding encryption keys or failing to rotate them compromises data confidentiality. | Use a Key Management Service (KMS) or secure environment variables. Implement key rotation policies. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup MVP | RBAC + Session Auth | Simple to implement, fast time-to-market. | Low development cost. |
| Enterprise Multi-tenant | ABAC + Org Scoping + Field Encryption | Granular control, compliance-ready, data isolation. | Higher complexity and compute overhead. |
| High Compliance (HIPAA/GDPR) | Field Encryption + Audit + Key Rotation | Meets regulatory requirements for data protection. | Significant infrastructure and operational cost. |
| Public API | JWT + Rate Limiting + Input Validation | Stateless scalability, abuse prevention. | Moderate infrastructure cost. |
Configuration Template
Use this template to configure security headers in your application middleware.
// src/middleware/security.ts
export const securityHeaders = {
'Content-Security-Policy': "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:;",
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
};
export function applySecurityHeaders(response: Response): void {
Object.entries(securityHeaders).forEach(([key, value]) => {
response.headers.set(key, value);
});
}
Quick Start Guide
- Initialize Identity Provider: Set up
better-auth with Argon2id hashing and secure cookie configuration.
- Deploy Access Control: Implement the
AccessControlEngine and integrate it into route handlers.
- Enable Data Vault: Instantiate
DataVault with a secure key and encrypt sensitive fields before storage.
- Add Middleware: Apply rate limiting, input validation, and security headers to all API routes.
- Configure Audit Logging: Set up logging for security events and integrate with your monitoring system.
By following this layered architecture, you establish a robust security posture that protects user data, prevents unauthorized access, and ensures compliance with industry standards. Security is an ongoing process; regularly review and update your configurations to address emerging threats.