# API Management

> How the platform's API gateway fronts a service — it validates the caller's token, injects identity headers, and attaches CORS — plus how a consumer calls a protected API. Apply to any service with exposesAPI.auth set, and to any consumer with a dependency (a `component`-kind sibling OR an `external`-kind upstream API) that calls a protected API. What the injected identity MEANS, and how to authorize on it, is owned by `thunder-authentication`.

- Skill: `wso2/api-management` (Agent Skill)
- Install (CLI): `npx skillmds@latest add wso2/api-management`
- Raw SKILL.md: https://api.skillmd.com/api/skills/wso2/api-management/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: wso2 (https://skillmd.com/u/wso2)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/wso2/api-management

---


# API Management

A service whose design sets `exposesAPI.auth` sits behind the platform's API
gateway. The gateway **terminates authentication**: it validates the caller's
token against the org's IDP and passes the verified identity down as headers.
Your code trusts those headers and never sees a token.

## Constraints

**Never validate a JWT.** The gateway did it already, against keys your service
does not have — the signing keys, the `client_id` and the IDP's discovery URL are
all platform-side. A service that parses or verifies tokens is duplicating the
gateway and will disagree with it.

**Never issue one either.** No `/auth/login`, `/auth/register`, `/auth/logout`,
or any token endpoint on any backend. The IDP owns token issuance — see
`thunder-authentication`.

**Identity arrives in headers**, set by the gateway from the validated token:

| Header | Claim | Presence |
|---|---|---|
| `X-User-Id` | `sub` | the caller's canonical, opaque IdP subject — always present on a protected request |
| `X-User-Groups` | `groups` | the caller's role groups, a JSON array — present when the user is in any group |
| `X-User-Name` | `username` | the caller's username — **may be absent** |
| `X-User-Ou` | `ouHandle` | the caller's organization (multi-tenant, optional) |

**`thunder-authentication` owns what these mean and how to authorize on them** —
role resolution, the directory join, and why `X-User-Id` is not a lookup key.
Two rules are this skill's, because they are the gateway's contract:

- **`X-User-Id` missing on a protected request → 401.** The gateway always sets
  it when it lets a request through, so its absence means the request did not
  come through the gateway — a deployment fault, not an anonymous caller. Declare
  the header OPTIONAL in your framework and resolve it in one helper, so your
  service picks that status: a framework-level "required header" rejection
  answers 400 before your resolver runs, which makes this rule unreachable.

- **Only the gateway may assert identity.** A proxy in front of your service that
  forwards untrusted traffic (a SPA's nginx) must clear inbound `X-User-*`, and
  must itself proxy THROUGH the gateway — `react-webapp` ships an asset that does
  both. A caller reaching your service on a lane with no gateway on it can set
  those headers freely.

- **A claim the token does not carry is not asserted.** The gateway writes a
  header only when its claim is present; when it is absent the client's own value
  for that header is forwarded. `groups` is the one that matters: a token issued
  without it (a `client_credentials` token, or a user in no groups) leaves
  `X-User-Groups` caller-controlled. Treat a role decision as trustworthy only
  for a caller whose token actually carries the claim. A service that owns its
  own people records sidesteps this: its role comes from the record it stored,
  keyed on `X-User-Id`, which no caller can set (`thunder-authentication`).
- **An authenticated caller who has no role → 403, never 401.** A 401 tells the
  SPA its token expired, so it restarts sign-in and loops forever. The role
  resolution itself is in `thunder-authentication`.

**Own your rows by `X-User-Id`.** It is the only stable per-caller key the
gateway gives you: stamp it on every row this service creates, and gate every
per-user query on it.

**CORS.** The `api-configuration` ClusterTrait attaches an Envoy CORS filter per
`visibility: external` HTTPRoute.

**Document the injected header.** In the OpenAPI you author for a protected
service, list `X-User-Id` under `parameters` so consumers know it is
required-but-injected: the gateway adds it, clients never set it.

## Implementation

Two rules, and both are mandatory in every protected handler:

1. **Read `X-User-Id`; 401 when it is missing.** Resolve it once, in one helper,
   rather than re-reading the header at each call site.
2. **Gate every per-user query on it — both filters, always.** A bare
   `WHERE id = ?` lets a caller reach any user's row by guessing its id; it must
   be `WHERE id = ? AND user_id = ?`. The same pairing applies to updates and
   deletes, and a query that matches nothing is a `404`, not a `500`.

Express both in your stack's own idiom — its routing style, where a shared
helper lives, and how a handler returns a status — following the conventions
that skill already sets rather than inventing a second one here.

Role-based and directory-scoped handlers build on this — see
`thunder-authentication`.

## Calling a protected upstream

When forwarding the caller's auth to an upstream `bearer` API, propagate the
inbound `Authorization` header verbatim — never re-issue or mint a token.

## Pitfalls

| Symptom | Cause | Fix |
|---|---|---|
| CORS error in the browser when calling this API | This service ships its own CORS middleware (doubled headers) | Remove the middleware. |
| Every protected request 401s in tests | Test calls carry no `X-User-Id` — in production the gateway sets it | Set `X-User-Id` directly on the request in tests; don't try to mint a JWT. |

