Skip to main content

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.
Development fallback accounts (admin123, broker123) exist in auth.ts and are guarded by if (process.env.NODE_ENV === 'production') return null. These credentials MUST NOT be active in production. See Development Hardcoded Credentials below.

Password Reset Flow

PensionsPortal.ie supports a secure self-service password reset flow for all user roles. Flow:
  1. User clicks “Forgot password?” on the login page.
  2. User submits their email address to POST /api/auth/forgot-password.
  3. Server generates a cryptographically secure 32-byte random token.
  4. Token is SHA-256 hashed before storage in password_reset_tokens table (plaintext never persisted).
  5. A reset email is sent via Resend with a link containing the plaintext token.
  6. User clicks the link and sets a new password via POST /api/auth/reset-password.
  7. Server validates the token hash, checks expiry (60 minutes), and updates the password.
  8. Token is marked as used (one-time use) to prevent replay.
Security Controls:
Code locations:
  • 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 via ActorContext. Two primary guard functions are used:

Role Hierarchy

Roles are encoded as an ordered enum. requireRole checks that roleLevel(actor.role) >= roleLevel(minimumRole).

Service Layer Enforcement

Every service method that touches protected data enforces both role and tenant:
See Multi-Tenancy Architecture for full details on tenant enforcement.

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_logs with 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

Store the output as 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

The audit_logs table has no application-layer DELETE or UPDATE operations. Database-level permissions restrict modification:
  • No DELETE privilege granted to the application database role
  • No UPDATE privilege 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

All POST 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

Rotating PPS_ENCRYPTION_KEY requires a migration to re-encrypt all existing PPS values. See the PPS Encryption Key Rotation Procedure in the Incident Response runbook. Do not rotate without following the procedure.

Development Hardcoded Credentials

auth.ts contains fallback development accounts for local testing:
These accounts:
  • Are completely disabled in production (guarded by NODE_ENV check)
  • 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_ENV guard
These credentials MUST NOT appear in production. Verify NODE_ENV=production is set in all production and staging deployment environments. The CI pipeline asserts this as part of the deployment checklist.

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 via next.config.js:

Compliance Cross-Reference