Overview
PensionsPortal.ie handles sensitive pension data for Irish occupational pension schemes. The platform is subject to IORP II (S.I. 128/2021), DORA, and GDPR compliance requirements. This document describes the security controls applied at every layer of the stack.Authentication
Auth.js v5 JWT Strategy
Authentication is handled by Auth.js v5 using the Credentials provider with a JWT session strategy. Sessions are stateless — no server-side session store is required.Password Hashing
All passwords are hashed with bcryptjs (bcrypt algorithm, work factor 12) before storage. Plaintext passwords are never persisted or logged.Password Reset Flow
PensionsPortal.ie supports a secure self-service password reset flow for all user roles. Flow:- User clicks “Forgot password?” on the login page.
- User submits their email address to
POST /api/auth/forgot-password. - Server generates a cryptographically secure 32-byte random token.
- Token is SHA-256 hashed before storage in
password_reset_tokenstable (plaintext never persisted). - A reset email is sent via Resend with a link containing the plaintext token.
- User clicks the link and sets a new password via
POST /api/auth/reset-password. - Server validates the token hash, checks expiry (60 minutes), and updates the password.
- Token is marked as used (one-time use) to prevent replay.
- API routes:
src/app/api/auth/forgot-password/route.ts,src/app/api/auth/reset-password/route.ts - Token schema:
src/db/schema/password-resets.ts - Email template:
src/lib/email/templates/password-reset.ts - UI pages:
src/app/auth/forgot-password/page.tsx,src/app/auth/reset-password/page.tsx
Authorization
Role-Based Access Control (RBAC)
Authorization is enforced at the service layer viaActorContext. Two primary guard functions are used:
Role Hierarchy
requireRole checks that roleLevel(actor.role) >= roleLevel(minimumRole).
Service Layer Enforcement
Every service method that touches protected data enforces both role and tenant:Transport Security
PPS Number Encryption (GDPR Priority 0)
Personal Public Service (PPS) numbers are highly sensitive personal identifiers under Irish GDPR. They receive the highest classification in the data protection model.Encryption Scheme
- Algorithm: AES-256-GCM (authenticated encryption — provides both confidentiality and integrity)
- Key size: 256-bit (32 bytes), stored as 64-character hex string in
PPS_ENCRYPTION_KEY - Storage: Encrypted ciphertext stored in
members.ppsNumberEncrypted— plaintext never persisted - IV: Randomly generated per encryption operation, prepended to ciphertext
Access Controls
- PPS numbers are never decrypted for display without an explicit broker action
- Every decryption event is written to
audit_logswith actor identity - PPS numbers are never sent to AI providers (Anthropic API receives only non-PII aggregate data)
- PPS numbers are never logged in application logs or error traces
Key Generation
PPS_ENCRYPTION_KEY in Vercel encrypted environment variables. Rotate on schedule (see Environment Variable Security).
Audit Logging (IORP II Requirement)
Audit logging is a regulatory requirement under IORP II (S.I. 128/2021) for pension scheme oversight. Every state-changing operation must be traceable.Audit Log Schema
Append-Only Guarantee
Theaudit_logs table has no application-layer DELETE or UPDATE operations. Database-level permissions restrict modification:
- No
DELETEprivilege granted to the application database role - No
UPDATEprivilege granted to the application database role - Reads are permitted for SuperAdmin and BrokerAdmin (own tenant only)
Audit logs are retained indefinitely per regulatory requirements. See Data Retention.
API Security
Authentication Gate
All/api/* routes (except /api/auth/* and /api/health/*) require an authenticated session. The Next.js middleware enforces this before any route handler runs:
Input Validation
AllPOST and PUT request bodies are validated with Zod schemas before any service method is called. Invalid input returns 400 Bad Request with a structured error:
SQL Injection Prevention
All database queries use Drizzle ORM with parameterised queries. String interpolation into raw SQL is not used anywhere in the codebase. Drizzle’s query builder compiles to parameterised prepared statements, eliminating SQL injection as an attack surface.AI Data Minimization
The Anthropic Claude API is used for compliance document drafting and regulatory Q&A (RAG). Strict data minimization is enforced:What Is Sent to Anthropic
RAG Corpus
The pgvector RAG corpus contains only public regulatory text — extracts from S.I. 128/2021 (IORP II transposition), Pensions Authority guidance, and Revenue Commissioner circulars. No member or scheme-specific data is embedded in the vector store.Dependency Security
Production runtime (the deployed Next.js application) has no known vulnerable packages. Vulnerabilities exist only in dev-time build tools.
npm audit is run on every CI build. PRs that introduce new production vulnerabilities are blocked by the CI pipeline.
Environment Variable Security
All secrets are stored in Vercel encrypted environment variables — never in source code or committed to git.Secrets Inventory
Development Hardcoded Credentials
auth.ts contains fallback development accounts for local testing:
- Are completely disabled in production (guarded by
NODE_ENVcheck) - Are documented as dev-only in code comments
- Use trivially guessable passwords intentionally (they never reach production)
- Are excluded from production builds via the
NODE_ENVguard
Cloudflare WAF
All production traffic passes through Cloudflare before reaching Vercel:
See
deployment/cloudflare.mdx for full WAF rule configuration.
Security Headers
The following security headers are set on all responses vianext.config.js: