8a78e759c0
- former-api-specs/mes: Alarm-API, MoveIn-MoveOut-API, API-key authgaps (from ~/Desktop/mesapi) - former-api-specs/dnc: Delmia-Integration-API — Delmia document service + WW recipe-download notify (from ~/Desktop/delmiaintegration) - known-issues: inbound API compile error not client-visible; no api-method validate
122 lines
6.7 KiB
Markdown
122 lines
6.7 KiB
Markdown
# 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).
|