Claims Validation
This document validates security claims made in compliance documentation against actual source code implementation.
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
https://developers.cloudflare.com/d1/reference/data-security/
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:
- Bookmark data security pages
- Download current Cloudflare SOC 2 report (via customer portal)
- 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.
- 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