Skip to main content

Authentication Security Features

Security hardening for authentication across DigiWedge services.

Summary

FeatureStatusNotes
Progressive lockoutComplete15m → 30m → 60m durations
Per-failure throttlingComplete+2s, +4s, +8s delays
Risk-based signalsCompleteIP/device/geo tracking
CSRF protectionCompleteDouble-submit cookies
Cookie-only tokensCompletePhase 5 complete; web + mobile
Structured error codesCompleteRefresh token failures
Refresh rate limitingCompletePer-IP + per-session limits
Account deletionCompleteGDPR self-service (#10496)

Lockout Policy

  • Progressive backoff windows after repeated failures.
  • Per-failure throttling to slow brute-force attempts.
  • Risk signals captured for IP, device, and geo anomalies.

CSRF Protection

  • Double-submit cookie pattern with XSRF-TOKEN.
  • SameSite cookies enforced for auth endpoints.

See Cookie-Only Sessions for infrastructure and implementation details.

Refresh Token Error Codes

Structured error codes for refresh token failures (#10450). The server returns specific codes in the error response for client-side handling.

CodeScenario
REFRESH_SESSION_NOT_FOUNDSession record not in database
REFRESH_TOKEN_INVALIDToken doesn't match stored hash
REFRESH_SESSION_REVOKEDSession explicitly revoked
REFRESH_SESSION_DELETEDSession has been deleted
REFRESH_SESSION_EXPIREDSession past expiry date
REFRESH_USER_NOT_FOUNDUser account not found
REFRESH_USER_INACTIVEUser account inactive/disabled

Client Usage

Frontend clients can extract the error code from the response:

import { RefreshErrorCode } from '@digiwedge/auth';

// In error handler:
const code = error.response?.data?.code;
if (code === RefreshErrorCode.SESSION_EXPIRED) {
// Handle expired session - redirect to login
}

The hooks-auth-web library's classifyAxiosError() function automatically extracts server-provided codes for consistent error handling.

Account Deletion (GDPR Article 17)

Self-service account deletion with OTP confirmation (#10496). Provides GDPR-compliant "right to erasure" and data portability.

Status: Complete (Backend + SDK + UI). All layers implemented including TeeTime Web, Mobile, and Admin UI.

Endpoints

EndpointMethodAuthRate LimitDescription
/auth/account/delete-requestPOSTJWT3/5minRequest deletion (sends OTP via email/WhatsApp)
/auth/accountDELETEJWT5/5minConfirm deletion with OTP code
/auth/account/restore-requestPOSTNone3/5min (per-IP)Request restore OTP (self-service)
/auth/account/restorePOSTNone5/5min (per-IP)Confirm restore with OTP code
/auth/account/exportGETJWT5/hourExport personal data (JSON or CSV)

Deletion Flow

  1. User requests deletion via POST /auth/account/delete-request
  2. OTP sent to verified email and WhatsApp (if verified)
  3. User confirms with OTP via DELETE /auth/account
  4. Account soft-deleted (deletedAt set), all sessions revoked
  5. 30-day grace period for restoration (configurable via ACCOUNT_DELETION_GRACE_DAYS)
  6. After grace period, permanent deletion via scheduled task

Self-Service Restore Flow

Users can restore their deleted account without authentication:

  1. User requests restore via POST /auth/account/restore-request with email
  2. OTP sent to deleted account's email (enumeration-resistant)
  3. User confirms with OTP via POST /auth/account/restore
  4. Account restored if within grace period

Export Data Format

The export endpoint supports both JSON (default) and CSV formats for GDPR Article 20 data portability compliance.

Query parameter: ?format=json (default) or ?format=csv

JSON Format

{
"exportedAt": "2026-01-04T00:00:00.000Z",
"user": { "id", "email", "createdAt", "status", "phoneVerified", "emailVerified" },
"profiles": [{ "provider", "givenName", "familyName", "createdAt" }],
"roles": [{ "id", "name", "description" }],
"tenants": [{ "id", "name", "slug" }],
"sessions": [{ "id", "createdAt", "device", "ipAddress" }],
"mfa": [{ "method", "verified", "createdAt" }],
"auditLog": [{ "action", "createdAt", "ipAddress" }]
}

CSV Format

Returns RFC 4180 compliant CSV with separate sheets for each data category:

{
"exportedAt": "2026-01-06T...",
"sheets": {
"user": "id,email,createdAt,status,...\nu-123,user@example.com,...",
"profiles": "provider,givenName,familyName,...",
"roles": "id,name,description",
"tenants": "id,name,slug",
"sessions": "id,createdAt,expiresAt,...",
"mfa": "method,verified,createdAt",
"auditLog": "action,createdAt,ipAddress,status"
}
}

Passwords, tokens, and other sensitive data are excluded from exports.

Security Features

  • Brute-force protection: OTP attempts are tracked and locked after 5 failed attempts
  • Per-IP rate limiting: Restore endpoints use explicit IP-based rate limiting (handles X-Forwarded-For)
  • Enumeration-resistant: All request endpoints return 204 regardless of user existence
  • Case-insensitive email: Restore flow uses case-insensitive email lookup for better UX
  • Audit logging: All deletion/restore events logged with IP address and user agent
  • Session revocation: All active sessions terminated on successful deletion
  • Token invalidation: All password reset tokens, MFA tokens, and OTPs invalidated on delete/restore
  • Pre-deletion validation: Downstream services can block deletion via event hooks (e.g., active bookings)

Notification Templates

Account lifecycle notifications use dedicated template types:

TemplatePurposeChannels
account_deletion_requestedOTP code for deletion confirmationEMAIL, SMS, WHATSAPP
account_deletedConfirmation with grace period infoEMAIL, SMS, WHATSAPP
account_restore_requestedOTP code for restore confirmationEMAIL, SMS, WHATSAPP
account_restoredRestoration success notificationEMAIL, SMS, WHATSAPP

Templates are validated at IDP startup via TemplateHealthCheckService. Missing critical templates (EMAIL channel) will log errors.

Seed templates: See docs/internal/runbooks/seed-auth-templates.md

Configuration

Environment VariableDefaultDescription
ACCOUNT_DELETION_GRACE_DAYS30Days before permanent deletion
ACCOUNT_DELETION_OTP_TTL_MIN15OTP code expiry in minutes
DEFAULT_COMMS_TENANT_IDglobalFallback tenant for notifications
ENABLE_RETENTION_CRON0Enable scheduled permanent deletion (1 to enable)
ACCOUNT_RETENTION_DAYS30Days after soft-delete before permanent deletion

Retention Cron Job

When enabled (ENABLE_RETENTION_CRON=1), a scheduled task runs daily at 2 AM to permanently delete users whose deletedAt timestamp exceeds the retention period:

  1. Finds eligible users: Queries users where deletedAt + ACCOUNT_RETENTION_DAYS < now
  2. Permanently deletes each user: Removes all related records (profiles, roles, tenants) in a transaction
  3. Emits events: Publishes USER_PERMANENTLY_DELETED event for each deletion
  4. Logs operations: Audit trail for GDPR compliance

Identity Events

Account lifecycle events are emitted for downstream service consumption (e.g., TeeTime Player, SCL Member cleanup):

EventTriggerPayload
identity.user.pre_deletion_validateBefore soft-deleteuserId, email, addBlockingReason() callback
identity.user.deletedAccount soft-deleteduserId, email, gracePeriodDays, permanentDeletionAt
identity.user.restoredAccount restoreduserId, email, originalDeletionAt
identity.user.permanently_deletedRetention cron deletesuserId, email, originalDeletionAt, daysInDeletedState

Pre-deletion validation (blocking deletion if conditions not met):

import { OnEvent } from '@nestjs/event-emitter';
import { IdentityEvents, PreDeletionValidatePayload } from '@digiwedge/auth';

@OnEvent(IdentityEvents.PRE_DELETION_VALIDATE)
async validateDeletion(payload: PreDeletionValidatePayload) {
const activeBookings = await this.bookingRepo.findActiveByUser(payload.userId);
if (activeBookings.length > 0) {
payload.addBlockingReason('TeeTime', 'User has active bookings');
}
}

Listening to lifecycle events (in downstream services):

import { OnEvent } from '@nestjs/event-emitter';
import { IdentityEvents, UserDeletedPayload } from '@digiwedge/auth';

@OnEvent(IdentityEvents.USER_DELETED)
async handleUserDeleted(payload: UserDeletedPayload) {
// Clean up related domain records (Player, Member, etc.)
}