ath fix
This commit is contained in:
+7
-5
@@ -18,6 +18,8 @@ These are known, deliberately-accepted Phase-1 exposures. Each has a compensatin
|
||||
| **AR-04** | **GRN-without-PO** — receive (and value) goods with no order; a fraud vector. | Emergency/direct receipt is useful (FR-GRN-02). | Permission-gated (when RBAC on); flagged for review; audit; cost entered here gets extra scrutiny (see C.3). | With RBAC / policy on direct receipts. |
|
||||
| **AR-05** | **In-transit loss window** — dispatched-but-not-received stock is untracked shrinkage risk. | Inherent to in-transit transfers (FR-STK-05). | In-transit aging monitoring report. | Add stuck-transfer alert (Part D). |
|
||||
| **AR-06** | **Localhost dev secrets** in `appsettings.Development.json`. | Local-dev convenience, current phase. | `.gitignore` + localhost only. | Before any shared/staging/prod → User Secrets / env vars; rotate. |
|
||||
| **AR-07** | **`getUserDetails` / `LogoutUser` callable without a bearer token** — `GET /api/v1/auth/users/{userId}` and `POST /api/v1/auth/logout` resolve the target user from the URL/payload, not the caller's session, so any anonymous caller can fetch a profile or log out an arbitrary user's sessions by GUID. | Carried over verbatim from AuthHex's own dispatcher contract (API_REFERENCE.md §3) — ERPCore's `AuthController` proxies it as-is rather than silently tightening a contract it doesn't own. | GUIDs are not enumerable; every call is written to `AuthEventLogs` upstream in AuthHex. | Revisit once AuthHex exposes a token-scoped variant, or add ERPCore-side rate limiting / auth requirement ahead of AuthHex. |
|
||||
| **AR-08** | **No rate limiting on `AuthController`'s anonymous endpoints** (login, register, refresh, recovery, OTP send/verify) — brute-force and account-enumeration exposure. | Not built in this pass (docs/11 §2.0, added 2026-07-16); AuthHex may rate-limit server-side but ERPCore does not add its own layer yet. | AuthHex's own lockout/backoff (per docs/10 NFR-03), immutable audit trail. | Add ASP.NET Core rate limiting middleware to `AuthController` before any non-local deployment. |
|
||||
|
||||
---
|
||||
|
||||
@@ -33,11 +35,11 @@ These are known, deliberately-accepted Phase-1 exposures. Each has a compensatin
|
||||
- [ ] Generic auth-failure messages (no account-enumeration signal)
|
||||
|
||||
### B.2 Token storage & CSRF *(httpOnly-cookie decision)*
|
||||
- [ ] Token in an **httpOnly, Secure** cookie (never localStorage) — removes XSS token theft
|
||||
- [ ] `SameSite=Strict` (or `Lax`) on the auth cookie
|
||||
- [ ] **CSRF protection on every state-changing request** (anti-forgery / double-submit token) — cookies are sent automatically, so CSRF is now in scope
|
||||
- [ ] CORS locked to the known frontend origin(s); credentials mode aligned with the cookie
|
||||
- [ ] Cookie scoped minimally (path/domain), Secure flag on
|
||||
- [x] Token in an **httpOnly, Secure** cookie (never localStorage) — removes XSS token theft (`AuthCookieWriter.WriteSession`, `erp_at`/`erp_rt`, 2026-07-16)
|
||||
- [x] `SameSite=Strict` on the auth cookie (`AuthCookieWriter`; assumes frontend + ERPCore share a registrable domain — revisit if deployed cross-domain)
|
||||
- [x] **CSRF protection on every state-changing request** (double-submit `XSRF-TOKEN` cookie + `X-XSRF-TOKEN` header, `ValidateCsrfAttribute`, applied to every mutating `AuthController` action; Bearer-header callers exempt since they aren't cookie-driven)
|
||||
- [ ] CORS locked to the known frontend origin(s); credentials mode aligned with the cookie — **not yet configured**; required before any browser frontend can call these endpoints cross-origin (tracked with the frontend-wiring follow-up)
|
||||
- [x] Cookie scoped minimally (path/domain), Secure flag on (`erp_rt` scoped to `/api/v1/auth/refresh-token`; all three cookies `Secure=true`)
|
||||
|
||||
### B.3 Audit integrity *(this is the compensating control for AR-01 — it must hold)*
|
||||
- [ ] Audit log **and** stock ledger are append-only **at the DB level** (the app's DB role has no `UPDATE`/`DELETE` on those tables)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> **Authoritative for:** backend architecture, business rules, and the data model (the 38-entity schema).
|
||||
> **Navigation:** you arrived here from `00-CORE.md`. API request/response contracts are in `11-BACKEND-PHASE1.md`. Frontend rules are in `20-FRONTEND.md`. Record work in `Backend/PROGRESS.md`.
|
||||
> **Scope basis:** SRS v1.1. Costing = FIFO · Multi-warehouse · Single-tenant · RBAC deferred (user identity stamped) · approvals auto/config-gated · vendor invoice + 3-way match deferred to Accounting.
|
||||
> **Authentication:** delegated to the **external AuthHex identity provider** (separate service). ERPCore is a **resource server** that only *validates* AuthHex's RS256 JWTs — it does not issue tokens or own a login endpoint. See A.4 (Authentication / Audit actor). RBAC (per-endpoint) still deferred.
|
||||
> **Authentication:** identity is owned by the **external AuthHex identity provider** (separate service), but as of 2026-07-16 the frontend no longer calls AuthHex directly — all login/registration/recovery/2FA/session traffic is proxied through ERPCore's own `AuthController` (`Controllers/AuthController.cs`, `Services/Auth/*`), which forwards to AuthHex and delivers the resulting session as httpOnly Secure cookies (docs/02-SECURITY.md §B.2). ERPCore still does not mint or sign tokens itself — it only forwards to and validates AuthHex's RS256 JWTs. See A.4 (Authentication / Audit actor) and `11-BACKEND-PHASE1.md §2.0` for the endpoint list. RBAC (per-endpoint) still deferred.
|
||||
|
||||
---
|
||||
|
||||
@@ -49,7 +49,8 @@ HTTP ─► Controller ─► Service ─► Repository ─► UnitOfWork / ErpD
|
||||
|
||||
## A.4 Cross-cutting
|
||||
- **Errors:** RFC 7807 `ProblemDetails` (framework default). Domain exceptions in `System/Errors` carry a stable `code`; a middleware maps them to `ProblemDetails`. Catalog in `11-BACKEND-PHASE1.md §7`.
|
||||
- **Authentication:** ERPCore is a **resource server**. It validates JWTs issued by the **external AuthHex IdP** — algorithm **RS256** (asymmetric RSA), issuer `AuthHex`, audience `AuthHexClient`. AuthHex exposes **no JWKS/OIDC discovery**, so ERPCore is configured with AuthHex's **RSA public key statically** (rotation is a manual config update). Tokens live ~1000 min (prod) / 60 min (dev). A single **door authorization policy** requires an ERP `UserTypeCode`/`RoleCode` claim (AuthHex is a shared IdP, so a valid token alone is not enough); **per-endpoint RBAC stays deferred**.
|
||||
- **Authentication:** ERPCore validates JWTs issued by the **external AuthHex IdP** — algorithm **RS256** (asymmetric RSA), issuer `AuthHex`, audience `AuthHexClient`. AuthHex exposes **no JWKS/OIDC discovery**, so ERPCore is configured with AuthHex's **RSA public key statically** (rotation is a manual config update). Tokens live ~1000 min (prod) / 60 min (dev). A single **door authorization policy** requires an ERP `UserTypeCode`/`RoleCode` claim (AuthHex is a shared IdP, so a valid token alone is not enough); **per-endpoint RBAC stays deferred**.
|
||||
- **Auth proxy:** `AuthController` (`Controllers/AuthController.cs`) is ERPCore's only endpoint group that talks to AuthHex over HTTP — via `IAuthHexClient` (`Infra/Auth/AuthHex/AuthHexClient.cs`, `AuthHex:BaseUrl` config) — and the only place that issues httpOnly `erp_at`/`erp_rt` session cookies (`Infra/Auth/AuthCookieWriter.cs`) plus the `XSRF-TOKEN` double-submit cookie checked by `ValidateCsrfAttribute` on mutating actions. The JWT bearer handler also accepts the `erp_at` cookie as a fallback (`JwtAuthExtensions`'s `OnMessageReceived`) when no `Authorization` header is present, so every other controller keeps working unchanged whether a caller sends a Bearer header or relies on the cookie session.
|
||||
- **Audit actor:** the token carries no `sub`/`nameid`; identity is AuthHex's custom **`UserId` (GUID)** claim. An `ICurrentUser` abstraction (`Infra/Auth`) resolves the acting user from a **local shadow user** — the GUID is mapped (JIT-provisioned) to a local `int` `users.user_id` that all FKs reference (see C.7). Services stamp mutations with it. **Never** trust a `createdBy` from the request body.
|
||||
- **Concurrency:** mutable resources carry a `RowVersion` (`[Timestamp] byte[]`), surfaced as `ETag`; `PUT`/`PATCH` require `If-Match` → `412` on mismatch.
|
||||
- **Numbering:** document numbers come from `NumberSequence` (per doc type, per year), issued inside the same transaction as the document.
|
||||
@@ -200,7 +201,7 @@ UI: responsive; count/pick screens handheld-friendly; status badges; mandatory-f
|
||||
|---|---|---|
|
||||
| NFR-01 | Performance | Single item/warehouse enquiry + valuation < 2s under normal load; ledger posting transactional, < 1s per line. |
|
||||
| NFR-02 | Integrity | FIFO layer consumption atomic and concurrency-safe; no double-consumption of remaining qty. |
|
||||
| NFR-03 | Security | Users authenticated via the external AuthHex IdP; **password hashing (BCrypt) is AuthHex's responsibility** — ERPCore validates tokens only. Every action attributed to a user and logged. (Role-based enforcement deferred, FR-X-01.) |
|
||||
| NFR-03 | Security | Users authenticated via the external AuthHex IdP, proxied through ERPCore's `AuthController` (docs/11 §2.0); **password hashing (BCrypt) is AuthHex's responsibility** — ERPCore forwards credentials and validates the resulting tokens only, never storing or hashing passwords itself. Every action attributed to a user and logged. (Role-based enforcement deferred, FR-X-01.) |
|
||||
| NFR-04 | Auditability | Audit trail immutable, retained per policy; ledger append-only. |
|
||||
| NFR-05 | Reliability | No stock transaction partially commits; full rollback on failure. |
|
||||
| NFR-06 | Scalability | Growth in items/warehouses/ledger without redesign; ledger indexed for time-series queries. |
|
||||
|
||||
+45
-10
@@ -18,7 +18,7 @@ Path-based versioning. Breaking changes bump the major version.
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
- Every endpoint requires a valid **Bearer JWT**; unauthenticated → `401`. Tokens are issued by the **external AuthHex IdP** (not ERPCore) — **RS256**, issuer `AuthHex`, audience `AuthHexClient`. ERPCore validates them against AuthHex's static RSA public key (no JWKS) and admits only holders of the configured ERP `UserType`/`Role` (door policy) → otherwise `403`.
|
||||
- Every endpoint requires a valid **Bearer JWT**, sent either as an `Authorization: Bearer <token>` header or as the `erp_at` httpOnly cookie issued by `AuthController` (§2.0); unauthenticated → `401`. Tokens are issued by the **external AuthHex IdP** (not ERPCore) — **RS256**, issuer `AuthHex`, audience `AuthHexClient`. ERPCore validates them against AuthHex's static RSA public key (no JWKS) and admits only holders of the configured ERP `UserType`/`Role` (door policy) → otherwise `403`.
|
||||
- **Per-endpoint RBAC is NOT enforced in Phase 1** (FR-X-01): any ERP-admitted user may call any endpoint.
|
||||
- The **audit actor** is AuthHex's custom **`UserId` (GUID)** claim, mapped to a local shadow user (`int`). Clients never send `createdBy`; the server derives it (docs/10 A.4).
|
||||
|
||||
@@ -59,15 +59,50 @@ Domain errors add a stable `code` (catalog §7):
|
||||
|
||||
## 2. Master Data
|
||||
|
||||
### 2.0 Auth — **external (AuthHex IdP); not an ERPCore endpoint**
|
||||
> **Superseded (2026-07-14).** ERPCore no longer exposes `/auth/login`. Login, registration and recovery are owned by the
|
||||
> separate **AuthHex** service (e.g. `POST /api/loginUser` with `{ identifier, password }`), which returns an **RS256** JWT
|
||||
> (issuer `AuthHex`, audience `AuthHexClient`; claims `UserId` (GUID), `UserTypeCode`, `RoleCode`, `NIC`, …). ERPCore only
|
||||
> **validates** that Bearer token and provisions a local shadow user (docs/10 A.4). The old shape is retained here for history:
|
||||
```json
|
||||
POST {AuthHex}/api/loginUser → { "identifier": "…", "password": "••••••••" }
|
||||
// 200 → an RS256 access token (Bearer). Bad credentials → 401. Token → ERPCore Authorization: Bearer <token>.
|
||||
```
|
||||
### 2.0 Auth — **`AuthController`, proxying the external AuthHex IdP**
|
||||
> **Superseding note (2026-07-16).** Un-superseded: the frontend no longer calls AuthHex directly. All of the endpoints
|
||||
> below live on ERPCore under `/api/v1/auth/*` (`Controllers/AuthController.cs`), each forwarding to the matching AuthHex
|
||||
> `functionName` (see the project-root `API_REFERENCE.md` for AuthHex's own contract) via `IAuthHexClient`. Session-issuing
|
||||
> endpoints deliver AuthHex's access/refresh tokens as **httpOnly Secure cookies** (`erp_at`, `erp_rt`) plus a non-httpOnly
|
||||
> `XSRF-TOKEN` cookie (docs/02-SECURITY.md §B.2) — response bodies never contain raw tokens. Mutating, cookie-authenticated
|
||||
> requests must echo the CSRF cookie value in an `X-XSRF-TOKEN` header or receive `403 CSRF_TOKEN_MISMATCH`; Bearer-header
|
||||
> callers (Swagger, service-to-service) are exempt. The JWT bearer handler also accepts the `erp_at` cookie in place of an
|
||||
> `Authorization` header (docs/10 A.4 Auth proxy), so every other `/api/v1/*` controller keeps working unchanged either way.
|
||||
|
||||
| Route | AuthHex function | Auth |
|
||||
|---|---|---|
|
||||
| `POST /api/v1/auth/register` | registerUser | Anonymous |
|
||||
| `POST /api/v1/auth/login` | loginUser | Anonymous |
|
||||
| `POST /api/v1/auth/login/otp/verify` | VerifyOtpForLogin | Anonymous |
|
||||
| `POST /api/v1/auth/refresh-token` | refreshToken | Anonymous (reads `erp_rt` cookie) |
|
||||
| `GET /api/v1/auth/users/{userId}` | getUserDetails | Anonymous* |
|
||||
| `GET /api/v1/auth/sessions` | getUserSessions | Required |
|
||||
| `POST /api/v1/auth/status` | ChangeUserStatus | Required + CSRF |
|
||||
| `POST /api/v1/auth/lock` | LockUserAccount | Required + CSRF |
|
||||
| `POST /api/v1/auth/change-password` | ChangeUserPassword | Required + CSRF |
|
||||
| `POST /api/v1/auth/verify-password` | VerifyPassword | Required |
|
||||
| `POST /api/v1/auth/logout` | LogoutUser | Anonymous* |
|
||||
| `PUT /api/v1/auth/me` | UpdateUser | Required + CSRF |
|
||||
| `POST /api/v1/auth/2fa/initiate` | initiateTwoFASetup | Required + CSRF |
|
||||
| `POST /api/v1/auth/2fa/complete` | completeTwoFASetup | Required + CSRF |
|
||||
| `POST /api/v1/auth/2fa/verify` | verifyTwoFA | Required + CSRF |
|
||||
| `POST /api/v1/auth/2fa/disable` | disableTwoFA | Required + CSRF |
|
||||
| `GET /api/v1/auth/2fa/status` | getTwoFAStatus | Required |
|
||||
| `POST /api/v1/auth/recovery/forgot-password` | forgotPassword | Anonymous |
|
||||
| `POST /api/v1/auth/recovery/verify-otp` | verifyOTP | Anonymous |
|
||||
| `POST /api/v1/auth/recovery/reset-password` | resetPassword | Anonymous |
|
||||
| `POST /api/v1/auth/recovery/reset-password-token` | resetPasswordWithToken | Anonymous |
|
||||
| `POST /api/v1/auth/availability` | IsAvailable | Anonymous |
|
||||
| `POST /api/v1/auth/otp/send` | sendOtp | Anonymous |
|
||||
| `POST /api/v1/auth/otp/verify` | VerifyOTP (Alt) | Anonymous |
|
||||
|
||||
\* `getUserDetails` and `LogoutUser` are anonymous because AuthHex itself resolves them from the request payload rather
|
||||
than the bearer token — carried over from AuthHex's own design, not introduced by this proxy. Tracked as an accepted risk
|
||||
in docs/02-SECURITY.md Part A.
|
||||
|
||||
Request/response field shapes match AuthHex's own payloads one-for-one (project-root `API_REFERENCE.md` §3–§5), except
|
||||
session-issuing responses omit `AccessToken`/`RefreshToken` (cookie-delivered instead) and `refreshToken` is read from the
|
||||
`erp_rt` cookie rather than the request body.
|
||||
|
||||
### 2.1 Items
|
||||
#### `GET /items`
|
||||
|
||||
Reference in New Issue
Block a user