This document is the source-of-truth for security practices in the ClipCash backend. It is intended to guide contributors as they add or modify API routes, background jobs, wallet flows, and realtime connections.
This project already applies several protections in code, including:
- JWT-based HTTP authentication with email-verification gating
- multi-factor authentication (MFA/TOTP) support
- CSRF protection for cookie-authenticated state-changing requests
- brute-force / account-lockout protection for login attempts
- rate limiting with the Nest throttler (Redis-backed, shared across instances)
- Helmet security headers
- explicit CORS configuration
- global request validation
- AES-256-GCM encryption for sensitive values
- socket authentication checks for websocket gateways
- security audit logging and request correlation IDs
- stack-trace stripping from all error responses
All new endpoints and integrations should follow the same standards described here. The implementation and the Swagger/OpenAPI contract are the expected security baseline for day-to-day development.
Every protected endpoint must follow the same contract:
- Require authentication at the server boundary, not only in the client.
- Use the existing JWT auth guard or a stricter guard for ownership- or role-scoped requests.
- Return standard
401 Unauthorizedfor missing/invalid/expired credentials. - Return standard
403 Forbiddenfor authenticated-but-not-authorized access. - Return standard
429 Too Many Requestswhen a rate limit is exceeded. - Mark protected routes in Swagger with
@ApiBearerAuth('access-token')and the relevant@ApiUnauthorizedResponse,@ApiForbiddenResponse, and@ApiTooManyRequestsResponsedecorators. - Apply validation and ownership checks before business logic.
- Keep secrets out of logs, responses, and exception payloads.
The backend uses JWT access tokens for authenticated API requests. The JWT strategy is configured in src/auth/strategies/jwt.strategy.ts and the app issues tokens from src/auth/auth.service.ts.
Signing configuration is in src/auth/auth.module.ts and is driven by environment variables from .env.example:
JWT_SECRET— secret used to sign access tokens. Defaults to a dev-only value; production must set a strong secret.JWT_EXPIRES— access-token lifetime in seconds (default3600/ 1 hour).
JWT claims (see AuthService.issueTokens):
sub— the authenticated user idemail— the user's email, if anyemailVerified— boolean;falseblocks access per the strategy
Required rules:
- Use
Authorization: Bearer <accessToken>for authenticated REST requests. - Access tokens must be signed with
JWT_SECRETand expire according toJWT_EXPIRES. - Do not accept unsigned or malformed tokens.
- Reject tokens when the user has not verified their email (
emailVerified === false) — enforced inJwtStrategy.validate. - Never log raw JWTs, refresh tokens, or authorization headers.
- Do not embed sensitive secrets, private wallet keys, PII, or role identifiers in JWT claims.
- Set
expiresInon the signing options so tokens cannot live indefinitely.
Refresh tokens:
- Refresh tokens are stored as opaque random values and hashed with SHA-256 before persistence.
- Tokens are bound to device metadata such as user agent hash and IP when available (
DeviceFingerprintService,src/auth/device-fingerprint.service.ts). - Refresh uses rotation: each call to
AuthService.refreshTokensrevokes the previous token (revokedAt) and issues a new pair. On a device-fingerprint mismatch the service revokes all of the user's outstanding refresh tokens and rejects the request with401 Unauthorized(potential session-hijack detection). - Expired, revoked, or already-used refresh/magic-link tokens must be rejected.
- Refresh/magic-link/password-reset tokens are single-use: a
usedAttimestamp marks them consumed.
MFA / TOTP:
- Accounts may opt in to TOTP two-factor authentication via
speakeasy. AuthService.setupMfagenerates a base32 secret and QR code;enableMfaverifies a submitted code before flippingmfaEnabled;disableMfaclears the secret.- On login, when
mfaEnabledis true, a valid TOTP code (dto.totpCode) is required and verified with a verification window of one step. Invalid codes yield401 Unauthorized. - MFA secrets are stored encrypted at rest through the encryption service and must never appear in responses or logs.
When adding a protected route:
- Use the existing auth guard and decorator patterns.
- Mark the endpoint in Swagger with
@ApiBearerAuth('access-token'). - Return standard
401 Unauthorizedwhen the JWT is missing, invalid, or expired.
The backend supports wallet-based ownership validation for blockchain actions. This is used to verify that a client controls a wallet address before allowing sensitive actions such as NFT mint authorization or wallet-linked actions.
Implementation expectations:
- Verify wallet signatures against the public key corresponding to the wallet address.
- Validate that the signed message is the expected challenge or canonical message for that endpoint.
- Reject malformed addresses, malformed signatures, or mismatched public keys.
- Do not trust a wallet address without proving ownership via signed challenge verification.
Multi-chain support (src/wallets/wallet-validation.service.ts):
- Stellar — Ed25519 public keys verified via
@stellar/stellar-sdkKeypair.fromPublicKey().verify(). - Solana — base58-encoded 32–44 byte Ed25519 addresses, format-validated.
- Base / EVM —
0x-prefixed 40-character hex addresses. - Use
validateAddressForChain(address, chain)to validate the correct format for the active chain.
Examples in this codebase:
src/wallets/wallet-validation.service.tsverifies Stellar Ed25519 signatures and validates Stellar, Solana, and EVM address formats.src/nft/mint-signature-verification.service.tsverifies signed mint authorization challenges.
Security rules for wallet operations:
- The server must not require private keys from clients.
- The signature must be checked on the server using the public key.
- Challenge strings should be deterministic and specific to the action being authorized (e.g.
ClipCash mint authorization for clip <clipId> by <walletAddress>). - Any wallet callback or signature verification must reject invalid payloads with
400 Bad Requestor401 Unauthorized, not silent success. - Replay protection: the canonical mint challenge is deterministic and therefore vulnerable to replay. Before mainnet launch, extend
MintSignatureVerificationServicewith a short-lived nonce (the code comment atsrc/nft/mint-signature-verification.service.tsrecommends storing a nonce in Redis) so each authorization can only be consumed once.
Wallet ownership guards:
src/wallets/guards/wallet-ownership.guard.tsandsrc/nft/guards/nft-ownership.guard.ts/nft-mint.guard.tsenforce ownership server-side. Apply these guards to any route where the caller claims a wallet or token owner.
Authorization is evaluated separately from authentication. A user may be authenticated but still not allowed to perform a sensitive action.
Required patterns:
- Use ownership guards or role checks before mutating records owned by another user.
- Check business ownership before allowing wallet, payout, NFT, clip, or admin actions.
- Return
403 Forbiddenwhen the caller is authenticated but not allowed to perform the action. - Prefer server-side ownership validation over trusting client-provided IDs.
Sensitive flows in this project include:
- wallet ownership checks
- clip ownership checks
- payout approvals
- admin-only routes
- protected revenue management
Rate limiting is enforced centrally through Nest throttler configuration in src/app.module.ts.
The app defines throttlers for:
- default
- auth
- authStrict
- sensitive
- emailVerify
- clipGenerate
- nftMint
- walletConnect
- walletDisconnect
- transactionSend
Example limits from the current implementation:
- default: 100 requests / 60 seconds
- auth: 10 requests / 60 seconds
- authStrict: 5 requests / 60 seconds
- sensitive: 3 requests / 15 minutes
- emailVerify: 3 requests / 60 minutes
- nftMint: 5 requests / 60 seconds
- transactionSend: 5 requests / 60 seconds
- Apply stricter limits to authentication, wallet, and transaction-heavy endpoints.
- Add
@Throttle(...)to endpoints that trigger expensive or sensitive actions. - Use the existing names and tiers instead of inventing ad hoc limits without reason.
- Reserve admin and internal endpoints for trusted networks or explicit allowlisting.
- Respect
THROTTLER_WHITELISTfor trusted IP addresses when needed.
Login attempts are tracked server-side by BruteForceProtectionService (src/auth/brute-force-protection.service.ts) using Redis, and gated by BruteForceGuard (src/auth/guards/brute-force.guard.ts).
Default configuration:
BRUTE_FORCE_MAX_ATTEMPTS— failed attempts before lockout (default5).BRUTE_FORCE_WINDOW_DURATION— rolling window for counting attempts (default900s).BRUTE_FORCE_LOCKOUT_DURATION— lockout duration (default900s).
Behaviour:
- Each failed login records an attempt; the Nth failure locks the account for the lockout duration.
- While locked, login for that email returns
423 Lockedwith body{ "error": "ACCOUNT_LOCKED", "lockoutTimeLeft": <seconds>, ... }. - The
SecurityAuditFilterlogsACCOUNT_LOCKOUTsecurity events for these responses. - Successful login clears all failed attempts for the user.
- Fail-open design: if Redis is unavailable, the service bypasses lockout (does not block login). This is a deliberate availability trade-off — monitor Redis health and the
Redis unavailable — brute-force protection bypassedlog warnings.
Apply the BruteForceGuard interceptor (or the brute-force check inside AuthService.login) to any credential-check endpoint to prevent password guessing and credential stuffing.### Rate-limit responses
When the request exceeds the configured limit, the API responds with:
429 Too Many Requests- standard Nest throttler error body
Example response:
{
"statusCode": 429,
"message": "ThrottlerException: Too Many Requests",
"error": "Too Many Requests"
}Swagger notes:
- Mark rate-limited endpoints with
@ApiTooManyRequestsResponse(...)where relevant. - Document the expected retry pattern (exponential backoff, respect
Retry-After/rate-limit headers when available).
The app configures Helmet in src/main.ts before routes are registered.
Current implementation includes:
Content-Security-PolicyStrict-Transport-SecurityX-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: no-referrerhidePoweredByremovalframeguarddenial
The app also disables x-powered-by explicitly and sets Cache-Control: no-store for non-API docs routes.
- Do not disable Helmet without a documented reason.
- Keep CSP restrictive for production.
- Allow Swagger UI inline scripts only when the docs are intentionally enabled.
- Avoid introducing untrusted inline scripts or active content in API responses.
- Never expose raw secrets in rendered HTML or documentation output.
CORS is configured in src/main.ts with app.enableCors(buildCorsOptions(...)).
Current behavior:
- No wildcard origin is used in the production allow-list logic.
- Development defaults allow localhost origins.
- Production accepts only origins defined in
ALLOWED_ORIGINS. - Methods and headers are explicitly controlled via environment variables.
Required practices:
- Keep
ALLOWED_ORIGINSexplicit and environment-driven. - Do not allow
*for credentialed browser requests. - Only expose the headers required by the frontend and API consumers.
- Prefer origin allow-listing over permissive cross-origin configurations.
State-changing requests (POST, PUT, PATCH, DELETE) that are authenticated via session/cookies must also carry a CSRF token. The guards live in src/csrf/:
CsrfService(src/csrf/csrf.service.ts) generates a 32-byte random token, sets it as the_csrfcookie, and validates token equality.CsrfGuard(src/csrf/csrf.guard.ts) and the inline middleware inCsrfModule.configure(src/csrf/csrf.module.ts) enforce the check on every non-GET/HEAD/OPTIONSrequest.
Required practices:
- Read the token from the
_csrfcookie and compare it againstX-CSRF-Token(header) or_csrf(body field). - Skip CSRF for
GET/HEAD/OPTIONSrequests only. - Skip CSRF for unauthenticated flows where no session is established:
/auth/signup,/auth/login,/auth/magic-link,/auth/verify-magic,/auth/verify-email,/auth/forgot-password,/auth/reset-password,/auth/google*,/api, and any request presenting anX-API-Keyheader (API-key auth is not vulnerable to the same cookie-based CSRF vector). - When a CSRF token is missing or does not match, respond
403 Forbiddenwith{ "message": "Invalid CSRF token" }. - The
_csrfcookie isSameSite=Strict,securein production, andHttpOnly: falseso the SPA can read it and send it back in a header.
For authenticated browser clients, the login response must issue the _csrf cookie via CsrfService.setCsrfCookie.
The application applies a global validation pipeline in src/main.ts:
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
})This means:
- unknown properties are stripped
- forbidden properties cause validation errors
- DTOs and types are transformed automatically where applicable
Required practices:
- Validate all request bodies, query params, and route params.
- Reject invalid IDs, invalid wallet addresses, invalid signatures, and malformed payloads.
- Use DTOs, not raw
req.bodyhandling, for public endpoints. - Prefer integer parsing and type-safe validation over trusting client input.
- Reject overlong or unsafe string values before they reach business logic.
Examples in this project:
@Param('id', ParseIntPipe)- wallet address validation via
WalletValidationService - password strength validation via custom validators
Sensitive data is encrypted using AES-256-GCM in src/encryption/encryption.service.ts.
Current implementation:
- Algorithm:
aes-256-gcm - Key derivation: SHA-256 of
ENCRYPTION_SECRET - Payload includes IV + auth tag + ciphertext
- Authentication tag is verified during decryption
This service is used for sensitive values such as:
- wallet secrets
- encrypted payout methods
- other persisted secret material
Required practices:
- Never store plaintext secrets in the database when they can be encrypted.
- Keep
ENCRYPTION_SECRETin environment or secret manager configuration only. - Rotate encryption keys according to the deployment lifecycle.
- Do not encrypt and then log the value or expose it in API responses.
Secrets must not be committed to source control.
Required environment variables include, but are not limited to:
JWT_SECRETENCRYPTION_SECRET- database credentials
- cloud, email, and blockchain provider credentials
- webhook secrets
Guidance:
- Store secrets in the deployment environment or a managed secret store.
- Never hardcode production secrets in code or examples.
- Fail startup if required secret configuration is missing for critical services.
- Rotate secrets after suspected exposure and review logs for accidental leakage.
The project includes realtime gateways for clip progress and video progress. These gateways validate JWTs before accepting a socket connection.
Examples:
src/clips/clips.gateway.tssrc/videos/video-progress.gateway.ts
Required practices:
- Require socket authentication on every connection for authenticated realtime streams.
- Accept a token only from the expected source: handshake auth, query param, or bearer Authorization header.
- Reject invalid or expired JWTs immediately and disconnect the socket.
- Scope events by user and room so one user cannot receive another user's progress updates.
- Keep CORS and origin restrictions tight for websocket gateways as well.
Examples from the current codebase:
- Socket connections verify JWTs before joining a room.
client.disconnect()is used on invalid or missing authentication.- Room names are user-scoped (for example
user:<userId>).
src/clips/clips.gateway.tsrestricts origins toALLOWED_ORIGINS(env-driven) withcredentials: true.- Caution:
src/videos/video-progress.gateway.tscurrently usescors: { origin: '*' }with no origin allow-listing. This is acceptable only while the gateway emits non-sensitive, user-scoped progress events, but it should be tightened to the sameALLOWED_ORIGINSallow-list before any authenticated subscription data lands on this namespace. Any new gateway must use the explicit allow-list, never'*'.
The backend should be treated as a production system and dependency hygiene matters.
Required practices:
- Keep dependencies updated and patch known vulnerabilities promptly.
- Periodically review
npm auditor equivalent security advisories. - Prefer well-maintained packages and avoid unnecessary integrations.
- Review third-party packages before introducing them to critical authentication or wallet flows.
- Treat webhooks and external service callbacks as untrusted inputs unless explicitly verified.
Important: all secrets, token material, and sensitive callback payloads should be validated before being trusted by application logic.
Every incoming HTTP request is assigned a correlation ID by RequestIdMiddleware (src/logger/request-id.middleware.ts) and attached to req.requestId.
- If the client/proxy supplies an
X-Request-Idheader it is reused; otherwise the API generates a UUID v4. - The same ID is echoed back on the
X-Request-Idresponse header and is appended torequestIdin error response bodies. - Logs (request start/complete, security events, 5xx errors) are all enriched with this
requestIdso a single value traces a full request lifecycle. RequestLoggingInterceptor(src/common/interceptors/request-logging.interceptor.ts) logs method, path, and duration only — never bodies, headers, or query parameters.
HttpExceptionLoggingFilter(src/common/filters/http-exception-logging.filter.ts) is registered globally and is the last filter to shape a response. It deletesstackandtracefrom every error body so internal details are never leaked to clients.- All error bodies include the
requestIdwhen available. SecurityAuditFilter(src/common/filters/security-audit.filter.ts) is also registered globally and inspects everyHttpException, logging structured security events for:401→AUTH_FAILURE403→PERMISSION_DENIED429→RATE_LIMIT_EXCEEDED423witherror === 'ACCOUNT_LOCKED'→ACCOUNT_LOCKOUT
- These audit log entries record method, path, IP, and
requestIdwithout request bodies or headers. Monitor this log stream for abuse or credential-stuffing patterns.
- Passwords are hashed with
bcrypt(cost factor10) inAuthService.signup/resetPassword(src/auth/auth.service.ts). Never store or log plaintext passwords. - Failed logins return generic
401 Unauthorized("Invalid credentials") whether or not the email exists, to prevent user enumeration. The message additionally reports remaining attempts; on lockout the account returns423 Locked.
The backend exposes the OpenAPI document in src/main.ts with SwaggerModule and configures bearer auth via addBearerAuth.
Protected endpoints should be explicitly marked as authenticated:
@ApiBearerAuth('access-token')@UseGuards(...)or the project auth decorator for protected routes@ApiUnauthorizedResponse({ description: 'Unauthorized' })
Examples already used in controllers such as wallet, clips, earnings, payouts, and transactions. For admin-only routes, also document the privilege requirement with @ApiForbiddenResponse({ description: 'Forbidden — admin access required' }).
When an endpoint is restricted by role or ownership, document it with:
@ApiForbiddenResponse({ description: 'Forbidden' })- clear endpoint summaries explaining who is allowed to call it
- ownership and scope rules in the endpoint description
- guard names or business rules in the summary when they affect API behavior
If an endpoint is rate-limited, add:
@ApiTooManyRequestsResponse({ description: 'Too Many Requests' })
This helps API consumers understand that retry behavior is expected under abuse protection. Sensitive routes such as auth, wallet connection, and transaction submission must advertise the throttle limit in the API contract.
For public and protected endpoints, apply the standard response patterns:
400 Bad Request— invalid payload or signature verification failure401 Unauthorized— missing or invalid auth credentials (JWT missing, invalid, or expired; unverified email)403 Forbidden— authenticated but not authorized; also returned for a missing/invalid CSRF token on browser-authenticated mutations423 Locked— account temporarily locked after repeated failed login attempts (ACCOUNT_LOCKED)429 Too Many Requests— throttled request
Representative response bodies:
{
"statusCode": 401,
"message": "Unauthorized",
"error": "Unauthorized"
}{
"statusCode": 403,
"message": "Forbidden",
"error": "Forbidden"
}{
"statusCode": 429,
"message": "ThrottlerException: Too Many Requests",
"error": "Too Many Requests"
}Document the applicable responses on each endpoint with @ApiBadRequestResponse, @ApiUnauthorizedResponse, @ApiForbiddenResponse, @ApiTooManyRequestsResponse, etc.
Before merging a new endpoint, verify that:
- It has the correct auth mode documented in Swagger.
- Protected endpoints use bearer auth metadata and/or guards.
- Sensitive operations are rate-limited.
- Authorization rules are enforced server-side, not only in the client.
400,401,403, and429cases are documented where relevant.- The endpoint does not log secrets or raw auth tokens.
- Input validation and ownership checks are implemented before business logic.
- If browser/cookie-authenticated, CSRF validation applies (see §4 CSRF protection).
- Account-lockout and security-audit events are covered for auth flows (see §9).
- Swagger marks the security scheme:
@ApiBearerAuth('access-token')for bearer-protected routes; CSRF-bearing routes document theX-CSRF-Tokenheader requirement.
When adding a new route, follow this checklist:
- Determine if the endpoint requires authentication.
- Determine if it is admin-only, owner-only, or user-scoped.
- Add the correct Nest guard(s).
- Add
@ApiBearerAuth(...)and the relevant Swagger error responses. - Add throttle protection for high-risk or expensive endpoints.
- Validate all input and reject malformed or unexpected values.
- Never trust client-provided ownership or wallet fields without verification.
- Keep secret values out of logs and API responses.
- Review the endpoint for CORS, cache, and security header implications.
- If browser-authenticated, ensure the CSRF middleware/guard covers it and clients send the
_csrftoken. - Ensure error responses strip stack traces and carry the correlation/request ID.
If a security issue is suspected:
- stop access to any compromised secret immediately
- rotate affected credentials and tokens
- review logs for leaked authorization data
- validate whether a wallet signature, JWT, or API credential was exposed
- report the issue through the project’s established security disclosure process, if available
Do not disclose private security details publicly without coordination.
The backend already implements a strong baseline for security, and contributors are expected to preserve and extend these controls. In practice, the most important rules are:
- protect APIs with JWT (and MFA/TOTP where enabled), email-verification gating, and authorization checks
- validate all inputs
- restrict CORS, add CSRF protection for cookie-authenticated mutations, and adopt strong headers
- enforce rate limiting and brute-force lockout on sensitive endpoints
- verify wallet ownership before trust
- encrypt secrets and avoid plaintext storage
- log security events with correlation IDs and never leak stack traces
- document real security guarantees in Swagger and contributor guidance
The commitment is simple: no endpoint should be considered safe unless authentication, authorization, validation, and rate limiting are all enforced by the server.