# Auth — current state: ScadaBridge

Repo: `~/Desktop/ScadaBridge`. Stack: .NET 10, Akka.NET; solution `ZB.MOM.WW.ScadaBridge.slnx`.
Auth code centers on the `ZB.MOM.WW.ScadaBridge.Security` project. All paths relative to repo root.
Verified 2026-06-01.

LDAP-centric identity with database-driven role mapping and **site-scoped** deployment;
multiple authenticated surfaces (Blazor UI, CLI, Management API, Inbound API).

## 1. Authentication

`src/ZB.MOM.WW.ScadaBridge.Security/`:
- `LdapAuthService.cs` — `AuthenticateAsync(username,password)` → `LdapAuthResult`. Direct LDAP bind (no Kerberos/NTLM). RFC 4514 DN escaping + LDAP filter escaping (injection-safe), username trim-normalization, per-operation socket timeout, distinct exception for service-account-bind failure vs bad user creds, **fails login if the group lookup fails** (won't admit with zero roles).
- `SecurityOptions.cs` (config section **`ScadaBridge:Security`**, flat keys): `LdapServer`, `LdapPort`, `LdapTransport` (**enum** `Ldaps`/`StartTls`/`None`), `AllowInsecureLdap`, `LdapSearchBase` (`dc=scadabridge,dc=local`), `LdapServiceAccountDn`, `LdapServiceAccountPassword`, `LdapUserIdAttribute` (`uid`; AD→`sAMAccountName`), `LdapDisplayNameAttribute`, `LdapGroupAttribute` (`memberOf`), `LdapConnectionTimeoutMs`, plus `JwtSigningKey`, `JwtExpiryMinutes` (15), `IdleTimeoutMinutes` (30), `JwtRefreshThresholdMinutes` (5), `RequireHttpsCookie`.
- `SecurityOptionsValidator.cs` — startup fail-fast if `LdapServer`/`LdapSearchBase` empty; `JwtSigningKey` ≥ 32 bytes.
- Dev LDAP: GLAuth in `infra/glauth/config.toml`, base `dc=scadabridge,dc=local`, groups `SCADA-Admins/Designers/Deploy-All/Deploy-SiteA`, users incl. `multi-role` / `admin` (pw `password`).

## 2. Authorization (roles + site scope — stays bespoke)

- Roles (`Security/Roles.cs`): `Admin`, `Design`, `Deployment`, `Audit`, `AuditReadOnly`.
- **Group→role is DB-driven** (not config): entity `Commons/Entities/Security/LdapGroupMapping.cs` (`LdapGroupName`→`Role`), repo `ConfigurationDatabase/Repositories/SecurityRepository.cs`, EF map `Configurations/SecurityConfiguration.cs`. `Security/RoleMapper.cs` → `MapGroupsToRolesAsync` returns roles + permitted site IDs + `isSystemWideDeployment` (union semantics: any unscoped Deployment mapping ⇒ system-wide).
- **Site-scoping:** `Commons/Entities/Security/SiteScopeRule.cs` (`LdapGroupMappingId`→`SiteId`). No scope rules on a Deployment mapping ⇒ all sites.
- ASP.NET policies (`Security/AuthorizationPolicies.cs`): `RequireAdmin/RequireDesign/RequireDeployment/OperationalAudit/AuditExport`.

## 3. Authenticated surfaces

| Surface | Entry | Mechanism | Role check | Site scope |
|---|---|---|---|---|
| Central UI (Blazor Server) | `/auth/login` form | LDAP → cookie | `[Authorize(Policy=…)]`, `AuthorizeView` | `CentralUI/Auth/SiteScopeService.cs` |
| CLI | `--username/--password` | HTTP Basic → Management API → LDAP | at Management API | at Management API |
| Management API | `POST /management` | HTTP Basic → LDAP | `ManagementActor.GetRequiredRole` then `Roles.Contains` | `ManagementActor.EnforceSiteScope` |
| Inbound API | `X-API-Key` header | **API key** hash lookup (not LDAP) | per-method approval in DB | n/a |
| Central↔Site | Akka ClusterClient + gRPC | none (cluster membership is the trust boundary; TLS in prod) | — | — |

Key files: `CentralUI/Auth/AuthEndpoints.cs` (`/auth/login`,`/auth/token`,`/auth/logout`,`/auth/ping`), `CentralUI/Auth/CookieAuthenticationStateProvider.cs`, `ManagementService/ManagementEndpoints.cs` + `ManagementActor.cs`, `CLI/ManagementHttpClient.cs`, `InboundAPI/ApiKeyValidator.cs`.

## 4. Session / identity model

Cookie auth (`Security/ServiceCollectionExtensions.cs`): cookie name `ZB.MOM.WW.ScadaBridge.Auth`, HttpOnly, SameSite=Strict, Secure conditional, sliding `IdleTimeoutMinutes` (30). Claims carry name/display/username/roles/site-ids. Separate **JWT** (`Security/JwtTokenService.cs`, HMAC-SHA256, issuer/aud `scadabridge-central`, 15-min expiry, refresh at 5-min threshold, idle-timeout enforced) used for programmatic/CLI via `/auth/token`. Shared `JwtSigningKey` across central nodes ⇒ no sticky sessions. Note: the cookie is the ASP.NET session token; JWT is *not* embedded in it.

**Inbound API keys** (`InboundAPI/ApiKeyValidator.cs`): `X-API-Key`, peppered HMAC-SHA256, **constant-time** compare, per-method approval, indistinguishable 403 for "no method"/"not approved".

## 5. Secrets & config

`ldap_login.txt` / `sql_login.txt` (real creds, git-ignored). Docker config in `docker/central-node-*/appsettings.Central.json`. Config hierarchy: `appsettings.json` → `appsettings.{Central|Site}.json` → env → CLI. Spec: `docs/requirements/Component-Security.md`.

## 6. Notable limits / TODOs

No Kerberos/NTLM; no dynamic role refresh (only on token expiry/LDAP re-query); no forced session revocation; `AddSecurityActors()` is a placeholder; no MFA; no password-reset (AD-managed).

---

## Adoption plan → `ZB.MOM.WW.Auth`

**Replace with the shared library:**
- `LdapAuthService` + `SecurityOptions` (LDAP portion) + `LdapAuthResult` → `ZB.MOM.WW.Auth.Ldap`. ScadaBridge's escaping/timeout/fail-closed hygiene is strong — fold it into the shared impl. Reconcile config: flat `ScadaBridge:Security:Ldap*` + `LdapTransport` enum vs the canonical schema in [`../../spec/SPEC.md`](../../spec/SPEC.md) (canonical uses the transport **enum**, which ScadaBridge already has — adopt ScadaBridge's `LdapTransport` shape, rename keys to canonical).
- `InboundAPI/ApiKeyValidator.cs` → `ZB.MOM.WW.Auth.ApiKeys` (same peppered-HMAC contract mxaccessgw extracts). Map "per-method approval" onto the contract's opaque constraint/policy hook.
- Cookie/JWT wiring → `ZB.MOM.WW.Auth.AspNetCore` (claims + cookie conventions). `JwtTokenService` is a candidate for a shared token helper if mxaccessgw/OtOpcUa want the same refresh model — otherwise keep bespoke.

**Keep bespoke:**
- Native role set (`Admin/Design/Deployment/Audit/AuditReadOnly`) + **site-scoping** (`SiteScopeRule`, union semantics) stay as the enforcement layer; implement `IGroupRoleMapper<CanonicalRole>` over the DB mapping and expand canonical→native. Per [`../../spec/CANONICAL-ROLES.md`](../../spec/CANONICAL-ROLES.md): `AuditReadOnly`→Viewer, `Audit`→Administrator (SoD collapse); no `Operator`/`Engineer`.
- `ManagementActor` role/scope enforcement, Akka cluster trust model.

**Note the group→role *mechanism* divergence:** ScadaBridge maps groups→roles in the **database**; OtOpcUa and mxaccessgw map in **config**. The shared seam (`IGroupRoleMapper`) must allow either backing store — see [`../../GAPS.md`](../../GAPS.md).
