Claims Validation

This document validates security claims made in compliance documentation against actual source code implementation.

Date: 2026-02-14
Version: Based on codebase analysis

Validation Summary

Claim Validated Evidence Notes
Password hashing (PBKDF2-SHA256) src/lib/crypto.ts:65-94 100,000 iterations
JWT signing (RS256) src/lib/jwt.ts:120, 154 jose library
MFA encryption (AES-GCM-256) src/lib/crypto.ts:140-186 256-bit key
TOTP (RFC 6238) src/services/mfa.service.ts:58-98 30-second period
Authorization code hashing src/services/oauth.service.ts:136 SHA-256
Timing-safe comparison src/lib/crypto.ts:198-209 Custom implementation
CSRF protection src/middleware/csrf.ts Double-submit cookie
SSRF protection src/lib/security.ts:276-333 Blocks private IPs
Input validation src/lib/security.ts:94-242 Email, length, XSS
D1 encryption at rest Platform-verified Cloudflare docs
KV encryption at rest Platform-verified Cloudflare docs

Password Hashing (SHA-256)

Claim

Passwords hashed using PBKDF2 with SHA-256

Status

✓ VALIDATED

Evidence

src/lib/crypto.ts:65-94

const PBKDF2_ITERATIONS = 100000;
const SALT_LENGTH = 16;

export async function hashPassword(password: string): Promise<string> {
  const salt = new Uint8Array(SALT_LENGTH);
  crypto.getRandomValues(salt);

  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    encoder.encode(password),
    'PBKDF2',
    false,
    ['deriveBits']
  );

  const derivedBits = await crypto.subtle.deriveBits(
    { name: 'PBKDF2', salt: salt, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
    keyMaterial,
    256
  );
  // ... returns `pbkdf2:${iterations}:${salt}:${hash}`
}

Validation

✓ PBKDF2 with 100,000 iterations and SHA-256

JWT Signing (RS256)

Claim

JWT signed using RS256 (RSA with SHA-256)

Status

✓ VALIDATED

Evidence

src/lib/jwt.ts:112-127

export async function generateAccessToken(
  env: Env,
  config: Config,
  payload: AccessTokenPayload
): Promise<string> {
  const privateKey = await getPrivateKey(env);

  const jwt = await new jose.SignJWT(payload)
    .setProtectedHeader({ alg: 'RS256', typ: 'at+jwt', kid: 'idpflare-key-1' })
    .setIssuer(config.urls.issuer)
    .setIssuedAt()
    .setExpirationTime(`${config.session.accessTokenDurationSeconds}s`)
    .setJti(crypto.randomUUID())
    .sign(privateKey);
  return jwt;
}

Validation

✓ RS256 algorithm using RSA key pair from JWT_PRIVATE_KEY/JWT_PUBLIC_KEY

MFA Secret Encryption (AES-GCM)

Claim

MFA secrets encrypted at rest using AES-GCM

Status

✓ VALIDATED

Evidence

src/lib/crypto.ts:140-186

export async function encrypt(plaintext: string, keyHex: string): Promise<string> {
  const key = await crypto.subtle.importKey(
    'raw', hexToBytes(keyHex), 'AES-GCM', false, ['encrypt']
  );

  const iv = new Uint8Array(12);  // 96-bit IV
  crypto.getRandomValues(iv);

  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv }, key, encoder.encode(plaintext)
  );

  // Returns `${ivHex}:${ciphertextHex}`
}

Usage

src/services/mfa.service.ts:152-154

const encryptedSecret = await encrypt(secret, env.ENCRYPTION_KEY);
const encryptedBackupCodes = await encrypt(JSON.stringify(backupCodes), env.ENCRYPTION_KEY);

Validation

✓ AES-GCM with 256-bit key (32-byte hex from ENCRYPTION_KEY)

TOTP Implementation (RFC 6238)

Claim

TOTP compliant with RFC 6238

Status

✓ VALIDATED

Evidence

src/services/mfa.service.ts:80-98

async function generateTOTP(secret: string, timestamp?: number): Promise<string> {
  const time = timestamp || Math.floor(Date.now() / 1000);
  const counter = Math.floor(time / 30); // 30-second period

  const key = base32Decode(secret);
  const counterBytes = intToBytes(counter);

  const hmac = await hmacSha1(key, counterBytes);

  // Dynamic truncation per RFC 6238
  const offset = hmac[hmac.length - 1] & 0x0f;
  // ... 6-digit code
}

Validation

✓ 30-second period, HMAC-SHA1, dynamic truncation

CSRF Protection

Claim

CSRF protection on state-changing operations

Status

✓ VALIDATED

Evidence

src/lib/security.ts:74-87, src/middleware/csrf.ts

export function generateCsrfToken(): string {
  const bytes = new Uint8Array(32);
  crypto.getRandomValues(bytes);
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

Validation

✓ Double-submit cookie pattern, cryptographically secure tokens

SSRF Protection

Claim

SSRF prevention for external hook URLs

Status

✓ VALIDATED

Evidence

src/lib/security.ts:276-333

export function isSafeExternalUrl(url: string): boolean {
  // Only allow HTTPS
  if (parsed.protocol !== 'https:') return false;

  // Block localhost and loopback
  if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') return false;

  // Block cloud metadata endpoints
  const blockedHosts = [
    '169.254.169.254', // AWS/GCP
    'metadata.google.internal',
    // ...
  ];

  // Block private IP ranges (RFC 1918)
  // 10.x.x.x, 172.16.x.x-172.31.x.x, 192.168.x.x
  // ...
}

Validation

✓ Blocks private IPs, metadata endpoints, requires HTTPS

Input Validation

Claim

Comprehensive input validation and sanitization

Status

✓ VALIDATED

Evidence

src/lib/security.ts:94-242

// Email validation
export function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

// Length limits
export const INPUT_LIMITS = {
  EMAIL_MAX_LENGTH: 254,
  PASSWORD_MAX_LENGTH: 128,
  NAME_MAX_LENGTH: 100,
  // ...
};

// XSS prevention
export function escapeHtml(unsafe: string): string {
  return unsafe
    .replace(/&/g, "&")
    .replace(//g, ">")
    .replace(/"/g, """)
    .replace(/'/g, "'");
}

Validation

✓ Email validation, length limits, XSS prevention, SQL injection prevention via parameterized queries

D1/KV Database Encryption at Rest

Claim

"Data at rest encrypted" via Cloudflare D1 and KV

Status

✓ VERIFIED - Cloudflare documents this

Cloudflare Documentation

D1 Database: "All objects stored in D1 are encrypted at rest"
https://developers.cloudflare.com/d1/reference/data-security/
KV Storage: "All values stored in KV are encrypted at rest with 256-bit AES-GCM"
https://developers.cloudflare.com/kv/reference/data-security/

What This Means

  • Encryption is automatic - no configuration required
  • Applies to all data (metadata, live/inactive databases for D1; all values for KV)
  • Encryption/decryption happens transparently through APIs
  • This is a platform-provided control - not visible in IDPFlare source code

For Audits

Use Cloudflare documentation links above as evidence:

  1. Bookmark data security pages
  2. Download current Cloudflare SOC 2 report (via customer portal)
  3. Reference specific documentation URLs in your compliance documentation

Auditor FAQ Preparation

Question Answer Evidence
How are passwords stored? PBKDF2 with 100,000 iterations and SHA-256. Source: src/lib/crypto.ts:65-94
What encryption is used for MFA secrets? AES-GCM with 256-bit key from ENCRYPTION_KEY. Source: src/lib/crypto.ts:140-186
How are JWTs signed? RS256 using RSA key pair. Source: src/lib/jwt.ts:120
Is data encrypted at rest? Application-level encryption (MFA secrets) uses AES-GCM. D1 database and KV storage encryption is provided by Cloudflare platform - refer to Cloudflare's security documentation.
Can access tokens be revoked? Access tokens are stateless JWTs with short TTL (1 hour default). Immediate revocation is not possible; use refresh token revocation and configure appropriate TTLs.
What prevents SSRF attacks? isSafeExternalUrl() blocks private IPs, localhost, and cloud metadata endpoints. Source: src/lib/security.ts:276-333

Cryptographic Properties

Component Algorithm Key Size Source
Password hashing PBKDF2 N/A (100k iter) crypto.ts:62
Password hash SHA-256 256-bit output crypto.ts:83
JWT signing RS256 2048-bit RSA jwt.ts:69
MFA encryption AES-GCM 256-bit crypto.ts:141
TOTP HMAC-SHA1 160-bit mfa.service.ts:59
Authorization codes SHA-256 256-bit hash oauth.service.ts:136
Refresh tokens SHA-256 256-bit hash oauth.service.ts:271
Random values CSPRNG - crypto.getRandomValues()

Conclusion

Most technical claims in the compliance documentation are validated against source code. The primary area requiring clarification is platform-provided encryption (D1/KV), which is outside IDPFlare codebase.

Action Items:
  • Update "encryption at rest" language to clarify platform dependency
  • Add access token revocation clarification
  • Add cookie security warning for proxy scenarios
  • Gather Cloudflare security documentation for audits