API-Key Injection at the Gateway

1. Overview

admin-service requires every request that arrives at /auth/ or at any portal-originated endpoint to carry a valid X-API-KEY header. The key authenticates the caller — specifically the portal gateway, not the user. User identity is carried separately, via the JWT on /api/ or via the claims in the token-exchange DTO.

The API key is held only on the gateway and only in two places: Spring Boot configuration (from a Kubernetes secret or environment variable) and a single @Value injection. It never enters Angular code, never reaches the browser, never appears in a log line.

This page covers how the key is injected on proxied requests, where it comes from, and how rotation works.

See also: Portal Pattern.

2. Why the API Key Exists

admin-service trusts the portal, not the browser. This trust is established by the portal proving ownership of a shared secret (the API key) on the subset of endpoints where browser identity alone is not sufficient:

  • POST /auth/token-exchange/oauth2 — accepts validated OIDC claims from the portal; admin-service must know that the claims came from a portal that already validated the IdP signature, not from an attacker posting arbitrary JSON.

  • POST /auth/external-login — accepts userKey + userHash; the portal proves it is allowed to submit external-auth attempts.

  • POST /auth/register-session — anonymous UUID exchange; portal proves it is a legitimate anonymous-flow front-door.

  • Any admin-service endpoint that accepts a request on behalf of the portal rather than a specific user.

The API key is admin-service’s answer to "who is calling me, and do I trust them enough to treat these claims as pre-validated?"

3. Injection Mechanisms

Portal gateways inject X-API-KEY in two places:

3.1. Spring Cloud Gateway route filter (proxied /services/admin-service/**)

Configured declaratively in application.yml:

spring:
  cloud:
    gateway:
      mvc:
        routes:
          - id: admin-service
            uri: ${application.admin-service.url}
            predicates:
              - Path=/services/admin-service/**
            filters:
              - RewritePath=/services/admin-service/(?<segment>.*), /$\{segment}
              - AddRequestHeader=X-API-KEY, ${application.admin-service.api-key}

Effect: every request under /services/admin-service/** is rewritten to drop the prefix and has X-API-KEY added before being proxied. The browser neither sees the key nor sees the target URL — the browser only talks to its own origin.

Equivalent programmatic form (RouteLocator bean) is available if more complex logic is needed; prefer the YAML form when the policy is static.

3.2. RestTemplate / WebClient for portal-initiated calls

The portal also makes direct server-to-server calls for token-exchange, cache invalidation, etc. These live in @Component classes like AdminServiceAuthClient and BackendTokenExchangeClient. Injection is explicit:

@Component
public class AdminServiceAuthClient {
    private final String apiKey;

    public AdminServiceAuthClient(
        RestTemplate restTemplate,
        @Value("${application.admin-service.url:http://localhost:12504}") String adminServiceUrl,
        @Value("${application.admin-service.api-key}") String apiKey
    ) {
        this.apiKey = apiKey;
        // ...
    }

    public String exchangeForJwt(...) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("X-API-KEY", apiKey);
        // ...
    }
}

Reference: [registration-portal/…​/service/AdminServiceAuthClient.java](registration-portal/src/main/java/za/co/idealogic/registration/ui/service/AdminServiceAuthClient.java).

The key is never passed as a method parameter across callers — it is injected once at construction and used privately. This prevents accidental logging via @Slf4j on a higher-level service that might print its arguments.

4. Configuration Surface

The key lives in the portal’s Spring config under:

application:
  admin-service:
    url: http://prod-event-admin-service
    api-key: ${ADMIN_SERVICE_API_KEY}   # resolved from env or K8s secret

Sourced in production via Helm-templated Kubernetes secret (see Helm Chart). Never committed to git.

Naming in the current codebase: the Helm chart surfaces it as config.services.apikey (see registration-portal Helm values). When admin-portal lands, follow the same key path so operators can reuse their mental model.

4.1. Admin-service side

admin-service validates via ApiKeyFilter which reads the key from its own config:

external-auth:
  secretKey: <admin-service side>

The two sides must match. Typical deployment holds the key in a shared Kubernetes secret and mounts it into both admin-service and each portal.

5. Rotation Procedure

Because the key is deployment-managed and not per-user, rotation is a deployment concern, not a user-facing one.

Rotate via rolling update:

  1. Generate a new key (any random 32+ char value).

  2. Update the admin-service config to accept both old and new keys for a short overlap window. This requires admin-service’s ApiKeyFilter to support a multi-key list; if it currently supports only one key, add the overlap capability before the first rotation.

  3. Roll admin-service pods to pick up the dual-key config.

  4. Update each portal’s Helm values to use the new key.

  5. Roll each portal’s pods.

  6. Once all portals are on the new key, remove the old key from admin-service’s config and roll admin-service again.

Total downtime: zero, assuming overlap support exists. If overlap support does not exist yet, it becomes a prerequisite ticket. Flag as an open item for WS5 / WS7.

5.1. Emergency rotation

If the key is known-compromised:

  1. Generate a new key.

  2. Deploy admin-service with only the new key — rejecting all in-flight requests carrying the old key.

  3. Deploy all portals with the new key in parallel. Expect a brief window of portal-level 401s; portals retry or users re-authenticate.

Emergency rotation accepts a few minutes of downtime in exchange for closing the compromised key immediately.

6. What Not To Do

  • Do not expose the API key to the Angular SPA. Not as environment.ts, not as a webpack-injected constant, not as a cookie. Anything reachable from browser JavaScript is assumed public.

  • Do not log the API key. Spring’s default configuration property masking should cover application.admin-service.api-key; verify with a GET /management/env check in a dev environment.

  • Do not reuse the same API key across unrelated deployments (prod + stage + dev). Use a per-environment key; generate fresh on each rotation.

  • Do not use the API key as a general authentication mechanism. It authenticates the portal to admin-service. User identity is separate (JWT or OIDC claims).

  • Do not accept the key from a request header on portal-local endpoints. The key flows portal → admin-service, never browser → portal.

7. Admin-portal Specifics

admin-portal’s Spring Cloud Gateway configuration is identical to registration-portal’s in shape — same /services/admin-service/** route, same AddRequestHeader=X-API-KEY. The only differences:

  • Separate key per portal (optional — admin-portal and registration-portal can use different keys for blast-radius reduction). Recommended when keys are cheap to manage (Kubernetes secrets).

  • admin-portal does not call /auth/external-login or /auth/register-session — only /auth/token-exchange/oauth2 for login + tenant switch. The AdminServiceAuthClient equivalent in admin-portal can be leaner.

8. Reference Implementation

File Role

registration-portal/src/main/resources/config/application.yml

Gateway route config with AddRequestHeader=X-API-KEY filter

registration-portal/src/main/java/…​/service/AdminServiceAuthClient.java

Example programmatic injection in a RestTemplate client

admin-service/src/main/java/…​/security/ApiKeyFilter.java

The admin-service side — validates incoming X-API-KEY

admin-service/src/main/java/…​/config/SecurityConfiguration.java

Which admin-service paths require the API key

10. Change History

Date Change

2026-04-24

Initial draft.