HTTP Security Headers
Every response from the auth service includes the following security headers:If you are running behind a reverse proxy (Nginx, Cloudflare, AWS ALB), make sure HSTS is not duplicated. The proxy typically handles TLS termination and HSTS, while the auth service adds the defense-in-depth headers.
Reverse Proxy Trust Boundary
Forwarded addresses affect IP-based abuse controls and security logs, so they must cross an explicit trust boundary.TRUST_PROXY defaults to false; in
that mode Fastify ignores X-Forwarded-For and uses the direct peer address.
Enable it only when network controls force every request through your known
proxy chain.
A numeric value trusts the closest 1 to 16 hops. Use it only when the path is
fixed and backend access is restricted to those proxies. If a shorter path can
reach the service, attacker-supplied entries may fall within the trusted hop
count. Prefer a comma-separated IP/CIDR allowlist for stable proxy addresses.
Blanket values (true, *, 0.0.0.0/0, and ::/0) are rejected.
Rate Limiting
The auth service applies fixed-window limits. Fastify enforces either its 5,000 req/min per-IP default or a route-specific Fastify override, while standard developer API-key requests also consume a Redis-backed plan budget. A standard-auth request must pass both its active Fastify policy and its plan policy.
A route-specific Fastify
config.rateLimit replaces the 5,000 req/min Fastify default for that route; it does not create a second Fastify layer. The Redis-backed standard developer plan budget remains additional whenever standard API-key authentication applies.
Commerce, the SCIM Bearer data-plane routes under /scim/v2/*, admin, and other custom-auth routes remain outside the standard developer plan bucket. Their active Fastify policy still applies. The standard API-key-authenticated /v1/scim/tokens management routes do consume the plan bucket.
When a client exceeds its limit, the API returns 429 Too Many Requests; Retry-After gives the minimum seconds to wait. Rate-limited responses expose X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds until reset). If Redis cannot complete the standard developer plan transaction, the request fails closed with 503 and RATE_LIMIT_UNAVAILABLE. The allow-listed JWKS endpoint is exempt and need not include those headers.
See the full Rate Limits guide for SDK integration examples and retry strategies.
Self-hosted tuning
Rate limits are configured inapps/auth-service/src/server.ts (Fastify default), apps/auth-service/src/plugins/dynamicRateLimit.ts (standard developer plan), and individual route files (Fastify overrides). A route override replaces the Fastify default, while the standard developer plan remains additional. Keep the default IP ceiling above the largest plan budget and share Redis across instances for the plan counters.
Active Fastify default/route counters are process-local; configure a shared store or equivalent ingress enforcement when deployment-wide Fastify ceilings are required:
CORS Policy
The auth service enables credentialed CORS only for exact origins listed in the comma-separatedCORS_ALLOWED_ORIGINS setting. Repository defaults allow https://grantex.dev, https://portal.grantex.dev, and http://localhost:5173; an empty value disables browser cross-origin access. Same-origin and non-browser callers do not send an Origin header and continue normally.
Self-hosted deployments should replace the repository defaults with the exact production origins they trust. Do not use wildcard origins with credentialed requests:
Admin API Authentication
The admin endpoints (/v1/admin/*) use a separate ADMIN_API_KEY environment variable. The key is validated against the Authorization: Bearer <key> header. If ADMIN_API_KEY is not set, all admin endpoints return 503 Service Unavailable.
SSO State Parameter
During SSO login flows (OIDC, SAML, LDAP), the auth service encodes session context (organization ID and connection ID) into astate parameter. The state is serialized as a base64url-encoded JSON payload and validated on callback to prevent CSRF attacks.
The SSO callback endpoints verify the state parameter structure before accepting any authentication response. Invalid or missing state values return 400 Bad Request.
JWT Expiry Validation
Every grant token is a RS256-signed JWT. Token verification (both online viaPOST /v1/tokens/verify and offline via verifyGrantToken()) always checks the exp claim. Expired tokens are rejected regardless of signature validity.
Default token lifetimes:
Scope Format Validation
Scopes follow theresource:action format and are validated at multiple points:
- Agent registration — scopes declared during
agents.register()are stored and used as the maximum allowable set - Authorization request — requested scopes must be a subset of the agent’s declared scopes
- Delegation — delegated scopes must be a subset of the parent grant’s scopes
400 Bad Request.
Input Sanitization
The auth service uses Fastify’s built-in JSON parser with strict validation. All request bodies must be valid JSON withContent-Type: application/json. Key protections:
- Request IDs — every request is assigned a
randomUUID()for tracing, attached asx-request-idin the response - SQL injection — all database queries use parameterized queries via
postgres.jstagged template literals (never string concatenation) - Type coercion — Fastify validates request parameters before route handlers execute
- Error messages — internal errors return generic messages; stack traces are never leaked to clients
Body Size Limits
Fastify enforces a default body size limit of 1 MB for all routes. This prevents denial-of-service attacks via oversized payloads. If a request exceeds the limit, Fastify returns413 Payload Too Large before the route handler runs.
For self-hosted deployments that need to accept larger policy bundles or compliance exports, increase the limit per-route:
API Key Hashing
API keys are never stored in plaintext. The auth service hashes every API key with SHA-256 before storing it in the database. Authentication works by hashing the incoming key and comparing it against stored hashes. This means:- Database breaches do not expose raw API keys
- API keys cannot be recovered — only rotated via
POST /v1/keys/rotate
Production Hardening Checklist
Rate limiting is enabled (Redis required)
CORS origins are restricted for browser-facing deployments
ADMIN_API_KEY is set to a strong random value
RSA_PRIVATE_KEY is a real 2048-bit key (not auto-generated)
TLS is terminated at the reverse proxy or load balancer
TRUST_PROXY is disabled or restricted to the verified proxy chainDatabase and Redis are on private networks, not publicly accessible
JWT_ISSUER matches your exact public URL
Grant tokens use short TTLs with refresh tokens for long workflows
Scopes follow least-privilege design with
resource:action formatAudit logging is enabled for all sensitive agent actions