# 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 <key>`, HTTP Basic with the key as the username, or — since
  `AllowInHttpParams` defaults on — `?apikey=<key>` 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=<key>` 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 <key>` | ✅ | ✅ — `Bearer sbk_<keyId>_<secret>` (the `Bearer ` prefix is optional; a bare token in `Authorization` also works) |
| `Authorization: Basic <base64("<key>:")>` (key as username) | ✅ | ❌ |
| `X-API-Key: <key>` | ❌ (not supported by stock ServiceStack) | ✅ — raw token `sbk_<keyId>_<secret>` |
| `?apikey=<key>` (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_<keyId>_<secret>` verified by a peppered-HMAC
  constant-time compare and **scoped to specific method names** (case-sensitive).
