# WWSupport MES API — API key authentication: support & gaps How the legacy **WWSupport / APIServer** ServiceStack service authenticates API-key callers, and the API-key-specific gaps in it **as of 2026-06-25** (the state of the `~/Desktop/mesapi` copy). Captured as reference for the ScadaBridge migration so the replacement Inbound API does not inherit these weaknesses. > Scope: defensive review of an internally-owned legacy service — weaknesses and remediations, no > exploit steps. Limited to API-key auth (other auth/config concerns are out of scope here). Source files: `APIServer/AppHost.cs`, `APIServer.ServiceInterface/MesServices.cs`, `APIServer/App.config`. --- ## What's supported (API key) - **`ApiKeyAuthProvider`** is registered in the host `AuthFeature` (`AppHost.cs:63-70`) with `SessionCacheDuration = 30 min` and `RequireSecureConnection = false`. - **Keys are DB-backed.** Users, roles, and keys live in SQL via `OrmLiteAuthRepository` (`UseDistinctRoleTables = true`); `InitSchema()` creates the tables at startup (`AppHost.cs:53-59`). There is no API-key config in `App.config` — keys are issued/stored by the ServiceStack auth repository. - **Authorization.** A valid key authenticates the request; the key's user must also hold the `MESAPI` role to reach any operation (`MesServices.cs:6-8`): ```csharp [Authenticate] [RequiredRole("MESAPI")] public class MesServices : Service { ... } // movein, moveout, alarmstatus, simplealarmstatus ``` - **Transports for the key** (ServiceStack defaults, nothing overridden): `Authorization: Bearer `, HTTP Basic with the key as the username, or — since `AllowInHttpParams` defaults on — `?apikey=` in the query string/form. - **Per-key environment tag.** A key whose `Environment == "test"` is routed to a `TestDb` connection instead of production (`AppHost.GetDbConnection`, `AppHost.cs:102-108`). - **Transport.** The listener is plain HTTP — `http://*:9501/` (DEV) / `http://*:9500/` (QA/PROD) (`App.config:57,68,80`); no HTTPS listener is configured (relevant to gap #1). --- ## Gaps & risks (worst first) ### 1. 🟠 High — the key is exposed in transit and in logs Three settings compound: - **No TLS** — plain-HTTP listeners (`App.config:57`) and `RequireSecureConnection = false` (`AppHost.cs:69`), so the key crosses the network in cleartext to any on-path observer. - **Key accepted in the URL** — `AllowInHttpParams` (default on) makes `?apikey=` valid, so keys land in proxy logs and browser history. - **Request logging persists it** — the enabled `RequestLogsFeature` writes request data to a CSV on disk (`AppHost.cs:79-86`), so a query-string key can be written to the log file. **Fix:** terminate TLS and set `RequireSecureConnection = true`; set `ApiKeyAuthProvider { AllowInHttpParams = false }` so keys must travel in the `Authorization` header; redact auth fields from the request log and restrict the log file's ACLs. ### 2. 🟡 Medium — keys are not scoped to methods A valid key + the `MESAPI` role grants access to **every** `[RequiredRole("MESAPI")]` endpoint (move-in, move-out, both alarm reads). There is no per-key allow-list of methods, so one leaked integration key exposes the entire MES surface. **Fix:** scope keys per integration (separate roles/keys for read vs. move-in vs. move-out), the way the ScadaBridge Inbound API does (keys carry an explicit method allow-list). ### 3. 🟢 Low–Medium — the per-key `test` redirect is half-wired `GetDbConnection` opens a `"TestDb"` connection for `Environment == "test"` keys (`AppHost.cs:104-106`), but **no `TestDb` connection string is defined** in `App.config` — such requests would throw. Separately, the move-out cycle-data read uses a raw `ConnectionStrings["BatchDB"]` connection (see `MoveIn-MoveOut-API.md`), so it ignores the redirect and reads production data even with a `test` key. **Fix:** define `TestDb` (or remove the redirect), and route the raw cycle-data read through the same environment-aware connection so a `test` key never touches production. --- ## Remediation checklist (priority order) - [ ] **Enforce TLS** + `RequireSecureConnection = true`; stop accepting the key in the URL (`AllowInHttpParams = false`); redact/secure the request logs (#1). - [ ] **Scope API keys per method/integration** instead of one coarse `MESAPI` role (#2). - [ ] **Fix or remove the `test` → `TestDb` redirect** and make the raw cycle-data read environment-aware (#3). --- ## Contrast: ScadaBridge Inbound API ScadaBridge uses an **`X-API-Key` header** on the data plane (`POST /api/{method}`), validated server-side, with each key **scoped to an explicit method allow-list** — versus one coarse `MESAPI` role granting all endpoints here. This file documents the legacy API-key posture so that difference is intentional and verifiable during cutover. ### Accepted auth transports | Transport | mesapi (ServiceStack `ApiKeyAuthProvider`) | ScadaBridge Inbound API | |-----------|--------------------------------------------|-------------------------| | `Authorization: Bearer ` | ✅ | ✅ — `Bearer sbk__` (the `Bearer ` prefix is optional; a bare token in `Authorization` also works) | | `Authorization: Basic :")>` (key as username) | ✅ | ❌ | | `X-API-Key: ` | ❌ (not supported by stock ServiceStack) | ✅ — raw token `sbk__` | | `?apikey=` (query string / form param) | ✅ (`AllowInHttpParams` defaults on) | ❌ — headers only | | Session cookie after first auth (`ss-id`/`ss-pid`) | ✅ (30-min `SessionCacheDuration`) | ❌ — stateless; every request re-presents the key | Evidence: mesapi is stock `new ApiKeyAuthProvider(AppSettings)` with no header customization (so ServiceStack defaults apply); ScadaBridge logic is `EndpointExtensions.cs:83-95`. Notes: - **The only common header is `Authorization: Bearer`** — the portable choice for a client that must talk to both. - **`X-API-Key` is ScadaBridge-only**; **`Basic` and `?apikey=` are mesapi-only.** A `curl -H "X-API-Key: …"` authenticates ScadaBridge but is *rejected* by mesapi. - **Precedence when two are sent:** ScadaBridge — `Authorization` wins over `X-API-Key`; mesapi — ServiceStack checks `Authorization` (Bearer/Basic) before the `apikey` param. - **mesapi is looser, ScadaBridge is tighter:** mesapi accepts the key in the URL and over a long-lived session cookie (more leak surface — see gap #1); ScadaBridge restricts to two headers, no URL param, no session. - **Token shape & verification:** mesapi keys are opaque ServiceStack keys checked against the auth repo; ScadaBridge keys are structured `sbk__` verified by a peppered-HMAC constant-time compare and **scoped to specific method names** (case-sensitive).