Authentication¶
api-test validates inbound credentials — the credentials the Plexara gateway forwards from its caller. The chain is:
- API key — header (default
X-API-Key) or query param (defaultapi_key). Validated against theapi_keys.filestatic list and, if enabled, the bcrypt-hashed Postgres store. - Static bearer —
Authorization: Bearer <token>matched against thebearer.tokensstatic list. - OIDC JWT —
Authorization: Bearer <jwt>validated against the configured IdP's JWKS. - Anonymous fallback — when
auth.allow_anonymous: trueand no credential matched, requests proceed with an anonymous identity.
The first authenticator that finds its credential type in the request decides the outcome. A bad credential does not fall through; the chain returns 401 immediately. This prevents accidental cross-mode matches (a typo'd JWT shouldn't accidentally pass the static-bearer list).
To verify the chain end-to-end, hit
GET /v1/whoami — it echoes the
resolved auth_type and subject, so you can confirm the credential
the gateway is actually sending. The auth pipeline diagram and the
data-flow notes live in Architecture › Auth chain.
File API keys¶
Simplest, no DB required.
auth:
allow_anonymous: false
api_keys:
header_name: "X-API-Key"
query_param_name: "api_key"
file:
- { name: "devkey", key: "${APITEST_DEV_KEY}", description: "default dev key" }
- { name: "ci", key: "${APITEST_CI_KEY}", description: "CI smoke tests" }
Lookup is constant-time (subtle.ConstantTimeCompare per row) so a
timing-side-channel attacker can't fingerprint which entry matched.
Postgres-backed API keys¶
Bcrypt-hashed; CRUD'd via the portal API. Slow per row (bcrypt is intentionally slow), so list scans cap at ~thousands of keys before they stop being practical. For a test fixture, that's fine.
The bcrypt store layers under the file store: file keys win, DB keys are consulted on miss. To create a key, use the portal or call the admin API directly:
curl -s -X POST http://localhost:8080/api/v1/admin/api-keys \
-H "X-API-Key: $APITEST_DEV_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"plexara-prod","description":"plexara production"}'
# → { "key": { "id": "...", "name": "plexara-prod", ... },
# "plaintext": "at_..." }
The plaintext value is shown once and never persisted. Capture it,
hand it to the gateway, and don't ask for it again.
Static bearer tokens¶
Mirror image of the API-key file store, against Authorization: Bearer.
bearer:
tokens:
- { name: "devbearer", token: "${APITEST_DEV_BEARER}", description: "default dev bearer" }
Used when a Plexara connection is configured with auth_mode: bearer
and credential: <token>.
OIDC JWT¶
When the Plexara gateway uses oauth2_client_credentials or
oauth2_authorization_code, it exchanges with the IdP and forwards the
resulting access token to api-test. api-test validates the JWT against
the configured IdP's JWKS.
oidc:
enabled: true
issuer: "http://keycloak.local:8081/realms/api-test"
audience: "api-test"
allowed_clients: ["plexara-cc", "plexara-ac"]
clock_skew_seconds: 30
jwks_cache_ttl: 1h
JWKS is cached in-process for jwks_cache_ttl. Validation checks:
- Signature against a key from the cached JWKS.
issmatchesoidc.issuer.audcontainsoidc.audience.azp(orclient_idclaim, depending on IdP) is inallowed_clients.expandnbfallow aclock_skew_secondstolerance.
The Keycloak realm dev/keycloak/api-test-realm.json pre-seeds
two confidential clients (plexara-cc for client-credentials,
plexara-ac for auth-code) and a portal user (dev / dev).
401 responses¶
Every 401 carries an RFC 6750 WWW-Authenticate header so an HTTP
client can discover the auth scheme:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api-test"
Content-Type: application/json
{"error":"missing credential"}
When the credential was supplied but invalid:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api-test", error="invalid_token"
Content-Type: application/json
{"error":"invalid credential"}
Anonymous mode¶
The chain still runs every authenticator; only when none match does it fall back to anonymous. So a bad credential still returns 401 — only absent credentials get the anonymous identity. This makes the fixture safe to run with anonymous + a few static keys: clients that send a valid key get their identity, clients that send nothing get anonymous, clients that send a bad key get 401.
Don't expect bad-credential demotion
allow_anonymous: true is not "let anything in." A typo'd API
key, an expired bearer token, or a JWT signed by the wrong key all
still return 401. The anonymous fallback only fires when there is
no credential header at all. If you want to allow truly
unauthenticated callers from a script while still allowing keyed
callers, make sure the script sends no X-API-Key or
Authorization header — not a placeholder.
Portal browser login¶
The portal uses a standard OIDC PKCE flow: hit /portal/, redirect to
the IdP, callback at portal.oidc_redirect_path, set a session cookie.
The portal API checks the cookie; everything else still uses the
inbound auth chain.