This document describes the API key authentication system implemented for TalentTrust Backend.
API keys provide a secure way for internal services and external integrations to access the TalentTrust API without requiring user authentication. API keys are separate from JWT user authentication and can be used for service-to-service communication.
API keys used for admin-only surfaces (DLQ inspection, deploy operations) require one of the following scopes:
| Scope | Permits |
|---|---|
deploy:* |
All deployment operations (switch, rollback, status) |
* |
Full access (admin keys only) |
jobs:admin |
DLQ list, replay, and metrics |
jobs:* |
All job-related admin operations |
The following endpoints require either a JWT with admin role or an API key with one of the admin scopes above:
GET /api/v1/jobs/dlq— List failed jobs in the DLQPOST /api/v1/jobs/dlq/reprocess— Replay a failed jobGET /api/v1/admin/deploy/status— Get deployment statePOST /api/v1/admin/deploy/switch-green— Promote green to activePOST /api/v1/admin/deploy/rollback— Roll back to blue
- Secure Generation: Cryptographically generated 32-byte hex keys
- Hashed Storage: Keys are hashed at rest using PBKDF2 with salt
- Scoping: Fine-grained permissions using resource:action format
- Constant-Time Comparison:
crypto.timingSafeEqualprevents timing attacks on key verification - Rotation: Safe key rotation without changing the key ID
- Expiration: Optional expiration dates for temporary access
- Audit Trail: Last usage tracking for security monitoring
- Deactivation: Secure deactivation of compromised or unused keys
API keys are 64-character hex strings:
abc123def456789012345678901234567890123456789012345678901234567890123456
API keys should be sent in the X-API-Key header:
GET /api/v1/contracts
X-API-Key: abc123def456789012345678901234567890123456789012345678901234567890123456API keys use a flexible scoping system with the following formats:
contracts:read # Can read contracts only
users:create # Can create users only
contracts:* # Can perform any action on contracts
*:read # Can read any resource
* # Can access everything (admin keys only)
POST /api/v1/api-keys
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"name": "Internal Service Key",
"scope": ["contracts:read", "contracts:create"],
"expiresAt": "2024-12-31T23:59:59Z"
}Response:
{
"message": "API key created successfully",
"apiKey": "abc123...",
"info": {
"id": "key-id",
"name": "Internal Service Key",
"scope": ["contracts:read", "contracts:create"],
"createdBy": "user-id",
"createdAt": "2024-01-01T00:00:00Z",
"expiresAt": "2024-12-31T23:59:59Z",
"isActive": true
}
}GET /api/v1/api-keys
Authorization: Bearer <jwt-token>Response:
{
"apiKeys": [
{
"id": "key-id",
"name": "Internal Service Key",
"scope": ["contracts:read", "contracts:create"],
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"expiresAt": "2024-12-31T23:59:59Z",
"lastUsedAt": "2024-01-15T10:30:00Z",
"isActive": true
}
],
"total": 1
}GET /api/v1/api-keys/:id
Authorization: Bearer <jwt-token>POST /api/v1/api-keys/:id/rotate
Authorization: Bearer <jwt-token>Response:
{
"message": "API key rotated successfully",
"apiKey": "def456...",
"info": {
"id": "key-id",
"name": "Internal Service Key",
"scope": ["contracts:read", "contracts:create"],
"createdBy": "user-id",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-15T10:30:00Z",
"expiresAt": "2024-12-31T23:59:59Z",
"isActive": true
}
}DELETE /api/v1/api-keys/:id
Authorization: Bearer <jwt-token>Response:
{
"message": "API key deactivated successfully"
}- API keys are hashed using PBKDF2 with 10,000 iterations
- Each key has a unique 16-byte salt
- Hashes are stored in the format
salt:hash - A deterministic
key_selector(SHA-256 of the plain key) is stored alongside the hash for O(1) indexed lookup; the selector alone cannot reveal the original key due to SHA-256 preimage resistance
- Rotation generates a new key while keeping the same ID
- Old keys become invalid immediately upon rotation
- No downtime during rotation process
- Optional expiration dates for temporary access
- Expired keys are automatically rejected
- Expired keys are deactivated on first access attempt
- Last usage timestamp is updated on successful authentication
- Helps identify unused or suspicious keys
- Useful for security monitoring and compliance
- Use descriptive names - Clearly identify the purpose of each key
- Apply minimal scope - Grant only necessary permissions
- Set expiration dates - Use temporary keys when possible
- Rotate regularly - Establish a rotation schedule for production keys
- Monitor usage - Review last usage timestamps regularly
- Never expose keys - API keys are only shown once during creation
- Use environment variables - Store keys securely in production
- Implement rate limiting - Protect against key abuse
- Monitor for anomalies - Set up alerts for unusual usage patterns
- Handle 401 errors - Gracefully handle invalid/expired keys
- Implement retry logic - Handle temporary network issues
- Log usage - Track which services use which keys
- Use appropriate scope - Request only necessary permissions
- Creation – A client calls the
POST /api/v1/api-keysendpoint. The server generates a cryptographically random 32‑byte key, hashes it with a unique salt using PBKDF2, stores thesalt:hashpair, and returns the plain‑text key once in the response. The plain key must be stored securely by the client; it is never persisted by the server. - Usage – Clients include the key in the
X‑API‑Keyheader on each request. The middleware extracts the header, verifies the key against the stored hash, updateslast_used_at, and enforces any required scopes viarequireApiKeyScope. - Expiration – If an
expires_attimestamp is set, the middleware rejects the key after that date, deactivates it on first use after expiry, and returns a 401 error. - Revocation – A key can be deactivated at any time via
DELETE /api/v1/api-keys/:id. Theis_activeflag is cleared, causing subsequent requests to be rejected with a 401. - Rotation – To rotate a key, call
POST /api/v1/api-keys/:id/rotate. A new plain‑text key is returned and the stored hash is replaced. The old key becomes immediately invalid. Update the consuming service's configuration with the new key, verify functionality, and optionally keep the old key for a short rollback window before fully deactivating it.
| Scope Pattern | Meaning |
|---|---|
resource:action |
Grants exactly the specified action on the given resource (e.g., contracts:read). |
resource:* |
Grants all actions on the specified resource (e.g., contracts:*). |
*:action |
Grants the action on any resource (e.g., *:read). |
* |
Grants full access; should only be used for admin keys. |
Note: Scopes are validated in
src/auth/apiKeyMiddleware.tsand must match one of the above patterns.
- Call the rotate endpoint and capture the new plain‑text key.
- Update the service configuration (env var, secret store) with the new key.
- Deploy the updated configuration.
- Verify that requests succeed with the new key.
- Monitor logs for any authentication failures.
- (Optional) Keep the old key active for a brief period to allow rollback, then deactivate it.
{ "error": "Missing X-API-Key header" }{ "error": "Invalid API key" }{ "error": "Forbidden: insufficient API key scope", "required": "contracts:read", "provided": ["users:read"] }{ "error": "Invalid request body", "required": { "name": "string", "scope": "string[]" } }- Algorithm: PBKDF2
- Iterations: 10,000
- Salt Length: 16 bytes (32 hex chars)
- Key Length: 64 bytes (128 hex chars)
- Hash Format:
salt:hash
The stored key_hash field must be a well-formed salt:hash string before PBKDF2 verification. This validation runs before calling the crypto functions to fail closed on malformed input (e.g., from botched migrations) rather than risking exceptions on the authentication hot path.
Validation rules:
| Check | Requirement |
|---|---|
| Non-empty | The stored value must not be empty |
| Separator | Must contain exactly one colon (:) |
| Parts | Must split into exactly 2 parts (no extra colons) |
| Salt length | Must be exactly 32 hex characters (16 bytes) |
| Hash length | Must be exactly 128 hex characters (64 bytes) |
On invalid format:
- Returns
null(rejects the key) - Does NOT throw exceptions
- Does NOT log the stored hash or salt (surfaces generic "invalid key" result)
- Preserves existing expiry and
last_used_atbehavior
Security notes:
- Timing-safe comparison (
timingSafeEqual) is preserved for valid keys - The validation runs before PBKDF2 to prevent potential denial-of-service from malformed input
- No information about the stored format is leaked in error responses
Every API key has an additional indexed field key_selector — a SHA-256 digest of the plain key that acts as a deterministic, non-reversible lookup key:
key_selector = SHA-256(plain_api_key)
Validation flow:
- Selector computation — On each request, compute
SHA-256(api_key)to derive the selector. - Indexed lookup — Query the storage layer by
key_selectorto find the candidate row in O(1) instead of scanning all stored keys. - Salted verification — The candidate's stored
salt:hashis verified with PBKDF2 (the same slow salted hash as before). This ensures the selector alone is insufficient to authenticate; an attacker who compromises the selector column still cannot derive the original key or bypass the salted hash. - Post-validation —
last_used_atis updated, expiry is checked (and the key deactivated if expired), and theApiKeyInfoshape is returned.
Security properties:
| Property | Mechanism |
|---|---|
| Selector preimage resistance | SHA-256 is one-way; key_selector cannot be reversed to the original key |
| Authenticator binding | PBKDF2 salted hash is still required — both selector match AND hash verification must pass |
| Timing safety | timingSafeEqual on the PBKDF2 comparison; selector lookups use the same constant-time index query |
| No downgrade | Existing O(n) fallback for legacy keys (those without key_selector) applies at most 1 PBKDF2 call per request, and the selector is backfilled automatically on first use |
Legacy migration: Keys created before this feature lack the key_selector field. On first successful validation they receive a backfilled selector, so subsequent requests hit the O(1) path.
interface ApiKey {
id: string;
name: string;
key_hash: string; // salt:hash format
key_selector?: string; // SHA-256 of the plain key (O(1) lookup index)
scope: string[];
created_by: string;
created_at: Date;
updated_at: Date;
expires_at?: Date;
last_used_at?: Date;
is_active: boolean;
}import { authenticateApiKey, requireApiKeyScope } from './auth/apiKeyMiddleware';
// API key authentication only
app.get('/api/internal', authenticateApiKey, handler);
// API key with scope validation
app.get('/api/contracts',
authenticateApiKey,
requireApiKeyScope('contracts', 'read'),
handler
);
// Either JWT or API key
app.get('/api/mixed',
authenticateEither,
handler
);- Identify service‑to‑service communication.
- Create API keys with appropriate scopes.
- Update clients to use
X-API-Keyheader. - Remove JWT authentication from service accounts.
- Monitor and test the new authentication flow.
- Generate new key using rotation endpoint.
- Update service configuration with new key.
- Test new key functionality.
- Deploy updated configuration.
- Monitor for any authentication failures.
- Keep old key temporarily for rollback.
- Key not working – Verify the key is copied correctly (no extra spaces), check expiration, ensure the key is still active, and verify required scope matches.
- Scope errors – Check exact scope format, ensure wildcards are used correctly, and verify the key has necessary permissions.
- Performance issues – Key validation is O(1) via the indexed
key_selectorfield; the slow PBKDF2 hash runs at most once per request. If you still see latency, check that all active keys have akey_selector(legacy keys fall back to an O(n) scan). Run the backfill or let lazy backfilling complete.
Enable debug logging to trace authentication flow:
// In development
console.log('API Key validation:', { keyId, scope, timestamp });For questions or issues with API key authentication:
- Check this documentation first.
- Review the implementation examples.
- Check the test files for usage patterns.
- Review error messages for specific issues.
- Contact the development team with detailed error information.