Files
ScadaBridge/docs/former-api-specs/mes/authgaps.md
T
Joseph Doherty 8a78e759c0 docs: former-api-specs (MES + DNC/Delmia) + inbound compile-error known issue
- 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
2026-06-26 04:13:19 -04:00

122 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 🟢 LowMedium — 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).