Skip to Content
ReferenceServicesAPI Gateway

API Gateway — Service Reference

Module: backends/api-gateway · Port: 8000 (REST, external ingress only)

For the route table, rate limits, and CORS port mapping, see API Gateway (concept). This page covers implementation mechanics that doc doesn’t.

Purpose

Spring Cloud Gateway (WebFlux, reactive) — the single external entry point for all client traffic. Owns TLS termination, JWT/session validation at the edge, Redis-backed rate limiting, CORS, and routing to Core Engine (:8080) or Admin Console (:8084). It never talks to gRPC microservices directly. Routes are registered programmatically in GatewayConfig.java (~1,195 lines), not via application.yml route YAML — most-specific first, ending in a catch-all /api/v1/**.

FileResponsibility
config/GatewayConfig.javaAll route definitions (auth requirement + rate-limit key per route)
config/JwtTokenValidationGatewayFilter.javaRejects requests without a valid JWT on a JWT-gated route
config/OptionalJwtTokenValidationGatewayFilter.javaValidates a JWT if present, doesn’t reject its absence (e.g. BYOK issuance, which can authenticate via signature instead)
security/JwtTokenValidator.javaHS512 signature/expiry validation + claims extraction (shared secret with core-engine, admin-console)
config/RateLimitingGatewayFilterFactory.javaRedis fixed-window rate limiter
config/RequestLoggingGatewayFilterFactory.javaCorrelation-ID injection/propagation, structured logging
config/IdentityHeaders.javaConstants for X-User-ID/X-User-Role/X-Issuer-ID/X-Token-Scope
config/ApiKeyHasher.javaHashes issuer API keys for the headless-issuer auth path
config/SecurityConfig.javaWebFlux security chain wiring
config/OpenApiConfig.javaAggregated OpenAPI/Swagger metadata
config/RedisConfig.javaReactiveRedisTemplate bean for the rate limiter
controller/FaviconController.javaSuppresses noisy 404s for /favicon.ico
exception/GatewayExceptionHandler.javaUniform JSON error envelope for gateway-level failures

JWT validation

JwtTokenValidator verifies HS512-signed tokens against a secret shared across three services — api-gateway, core-engine, admin-console — via the same JWT_SECRET env var. All three must use the identical secret or cross-service tokens silently fail validation.

On success the gateway sets these headers on the proxied request (client-supplied copies are stripped first, so downstream services can trust them):

X-User-ID X-User-Role X-Issuer-ID (when present) X-Token-Scope

X-Token-Scope disambiguates an admin-console platform admin from an issuer-portal user who also holds an "Admin" role claim, since both are minted with the same shared secret from overlapping role vocabularies. JwtTokenValidator.PENDING_APPROVAL_SCOPE ("issuer-portal-pending") is a special scope core-engine mints for an issuer still PENDING_APPROVAL (or rejected); every JWT-gated filter must reject this scope except the public /api/v1/issuer/auth/** endpoints.

Holder and verifier sessions are not gateway-validated bearer JWTs — they’re HttpOnly cookies validated by Core Engine itself. The gateway routes those paths through as Cookie session auth (no gateway-side JWT filter).

Rate limiting

RateLimitingGatewayFilterFactory is a Redis-backed fixed 60-second window counter (INCR + EXPIRE 60s per key — resets at the window boundary, not a true sliding window despite the class-level Javadoc). Key format: ratelimit:<limitKey>:<clientId>.

clientId resolves in priority order:

  1. X-User-ID header (set by the JWT filter for an authenticated caller)
  2. X-Issuer-ID header, prefixed issuer: (for issuer-scoped routes)
  3. X-Forwarded-For / remote socket address (anonymous/public callers)

Every response gets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Exceeding the limit returns 429 Too Many Requests with Retry-After: 60 and a JSON error body.

Fail-open by design: .onErrorReturn(true) means a Redis outage disables rate limiting entirely rather than blocking traffic — a deliberate availability-over-strictness tradeoff, worth knowing when reasoning about abuse scenarios during a Redis incident.

CORS

Configured in application.yml (spring.cloud.gateway.server.webflux.globalcors), not in Java. Two exact-path exemptions registered before the /** catch-all:

  • /api/v1/issuer/auth/sso/saml/acs — issuer SAML ACS webhook
  • /api/v1/custodian/auth/sso/saml/acs — custodian SAML ACS webhook

Both allow any origin with credentials disabled — a SAML ACS endpoint is a cross-origin POST from the identity provider’s own domain by design; the real security boundary is SamlAssertionValidator’s XML-DSig signature check, not the Origin header.

Catch-all /** default CORS_ALLOWED_ORIGINS:

http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:5176,http://localhost:3000

— issuer-portal (5173), verifier-portal (5174), admin-console (5175), credential-wallet (5176), plus a generic 3000 fallback.

Known gap: custodian-portal’s dev port (5178) is absent from this default allowlist, even though the gateway has an entire custodian route surface (custodian-registration, custodian-auth, custodian-profile, custodian-intake, custodian-erasure-requests, custodian-credentials, custodian-holders, custodian-workforce*, custodian-compliance*, custodian-team, custodian-search, custodian-admin-ops — all present in GatewayConfig.java). A deployed environment typically overrides CORS_ALLOWED_ORIGINS anyway, which is presumably why this hasn’t surfaced — but a bare ./gradlew :backends:api-gateway:bootRun + cd frontends/custodian-portal && npm run dev combination hits CORS failures on the default config alone.

OpenAPI aggregation

OpenApiConfig publishes a single aggregated OpenAPI bean (“Attest ID - Credential Management API”), served via springdoc at /v3/api-docs / /swagger-ui.html. Documents Development (localhost:8000) and Production (api.attestpro.com) servers. This is a hand-written top-level description, not a mechanically-merged aggregation of each backend’s own OpenAPI spec — the gateway self-describes rather than proxying/merging Core Engine’s and Admin Console’s own generated specs.