163 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			163 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # StellaOps Authority Service
 | ||
| 
 | ||
| > **Status:** Drafted 2025-10-12 (CORE5B.DOC / DOC1.AUTH) – aligns with Authority revocation store, JWKS rotation, and bootstrap endpoints delivered in Sprint 1.
 | ||
| 
 | ||
| ## 1. Purpose
 | ||
| The **StellaOps Authority** service issues OAuth2/OIDC tokens for every StellaOps module (Feedser, Backend, Agent, Zastava) and exposes the policy controls required in sovereign/offline environments. Authority is built as a minimal ASP.NET host that:
 | ||
| 
 | ||
| - brokers password, client-credentials, and device-code flows through pluggable identity providers;
 | ||
| - persists access/refresh/device tokens in MongoDB with deterministic schemas for replay analysis and air-gapped audit copies;
 | ||
| - distributes revocation bundles and JWKS material so downstream services can enforce lockouts without direct database access;
 | ||
| - offers bootstrap APIs for first-run provisioning and key rotation without redeploying binaries.
 | ||
| 
 | ||
| Authority is deployed alongside Feedser in air-gapped environments and never requires outbound internet access. All trusted metadata (OpenIddict discovery, JWKS, revocation bundles) is cacheable, signed, and reproducible.
 | ||
| 
 | ||
| ## 2. Component Architecture
 | ||
| Authority is composed of five cooperating subsystems:
 | ||
| 
 | ||
| 1. **Minimal API host** – configures OpenIddict endpoints (`/token`, `/authorize`, `/revoke`, `/jwks`) and structured logging/telemetry. Rate limiting hooks (`AuthorityRateLimiter`) wrap every request.
 | ||
| 2. **Plugin host** – loads `StellaOps.Authority.Plugin.*.dll` assemblies, applies capability metadata, and exposes password/client provisioning surfaces through dependency injection.
 | ||
| 3. **Mongo storage** – persists tokens, revocations, bootstrap invites, and plugin state in deterministic collections indexed for offline sync (`authority_tokens`, `authority_revocations`, etc.).
 | ||
| 4. **Cryptography layer** – `StellaOps.Cryptography` abstractions manage password hashing, signing keys, JWKS export, and detached JWS generation.
 | ||
| 5. **Offline ops APIs** – internal endpoints under `/internal/*` provide administrative flows (bootstrap users/clients, revocation export) guarded by API keys and deterministic audit events.
 | ||
| 
 | ||
| A high-level sequence for password logins:
 | ||
| 
 | ||
| ```
 | ||
| Client -> /token (password grant)
 | ||
|   -> Rate limiter & audit hooks
 | ||
|   -> Plugin credential store (Argon2id verification)
 | ||
|   -> Token persistence (Mongo authority_tokens)
 | ||
|   -> Response (access/refresh tokens + deterministic claims)
 | ||
| ```
 | ||
| 
 | ||
| ## 3. Token Lifecycle & Persistence
 | ||
| Authority persists every issued token in MongoDB so operators can audit or revoke without scanning distributed caches.
 | ||
| 
 | ||
| - **Collection:** `authority_tokens`
 | ||
| - **Key fields:**
 | ||
|   - `tokenId`, `type` (`access_token`, `refresh_token`, `device_code`, `authorization_code`)
 | ||
|   - `subjectId`, `clientId`, ordered `scope` array
 | ||
|   - `status` (`valid`, `revoked`, `expired`), `createdAt`, optional `expiresAt`
 | ||
|   - `revokedAt`, machine-readable `revokedReason`, optional `revokedReasonDescription`
 | ||
|   - `revokedMetadata` (string dictionary for plugin-specific context)
 | ||
| - **Persistence flow:** `PersistTokensHandler` stamps missing JWT IDs, normalises scopes, and stores every principal emitted by OpenIddict.
 | ||
| - **Revocation flow:** `AuthorityTokenStore.UpdateStatusAsync` flips status, records the reason metadata, and is invoked by token revocation handlers and plugin provisioning events (e.g., disabling a user).
 | ||
| - **Expiry maintenance:** `AuthorityTokenStore.DeleteExpiredAsync` prunes non-revoked tokens past their `expiresAt` timestamp. Operators should schedule this in maintenance windows if large volumes of tokens are issued.
 | ||
| 
 | ||
| ### Expectations for resource servers
 | ||
| Resource servers (Feedser WebService, Backend, Agent) **must not** assume in-memory caches are authoritative. They should:
 | ||
| 
 | ||
| - cache `/jwks` and `/revocations/export` responses within configured lifetimes;
 | ||
| - honour `revokedReason` metadata when shaping audit trails;
 | ||
| - treat `status != "valid"` or missing tokens as immediate denial conditions.
 | ||
| 
 | ||
| ## 4. Revocation Pipeline
 | ||
| Authority centralises revocation in `authority_revocations` with deterministic categories:
 | ||
| 
 | ||
| | Category | Meaning | Required fields |
 | ||
| | --- | --- | --- |
 | ||
| | `token` | Specific OAuth token revoked early. | `revocationId` (token id), `tokenType`, optional `clientId`, `subjectId` |
 | ||
| | `subject` | All tokens for a subject disabled. | `revocationId` (= subject id) |
 | ||
| | `client` | OAuth client registration revoked. | `revocationId` (= client id) |
 | ||
| | `key` | Signing/JWE key withdrawn. | `revocationId` (= key id) |
 | ||
| 
 | ||
| `RevocationBundleBuilder` flattens Mongo documents into canonical JSON, sorts entries by (`category`, `revocationId`, `revokedAt`), and signs exports using detached JWS (RFC 7797) with cosign-compatible headers.
 | ||
| 
 | ||
| **Export surfaces** (deterministic output, suitable for Offline Kit):
 | ||
| 
 | ||
| - CLI: `stella auth revoke export --output ./out` writes `revocation-bundle.json`, `.jws`, `.sha256`.
 | ||
| - Verification: `stella auth revoke verify --bundle <path> --signature <path> --key <path>` validates detached JWS signatures before distribution, selecting the crypto provider advertised in the detached header (see `docs/security/revocation-bundle.md`).
 | ||
| - API: `GET /internal/revocations/export` (requires bootstrap API key) returns the same payload.
 | ||
| - Verification: `stella auth revoke verify` validates schema, digest, and detached JWS using cached JWKS or offline keys, automatically preferring the hinted provider (libsodium builds honour `provider=libsodium`; other builds fall back to the managed provider).
 | ||
| 
 | ||
| **Consumer guidance:**
 | ||
| 
 | ||
| 1. Mirror `revocation-bundle.json*` alongside Feedser exports. Offline agents fetch both over the existing update channel.
 | ||
| 2. Use bundle `sequence` and `bundleId` to detect replay or monotonicity regressions. Ignore bundles with older sequence numbers unless `bundleId` changes and `issuedAt` advances.
 | ||
| 3. Treat `revokedReason` taxonomy as machine-friendly codes (`compromised`, `rotation`, `policy`, `lifecycle`). Translating to human-readable logs is the consumer’s responsibility.
 | ||
| 
 | ||
| ## 5. Signing Keys & JWKS Rotation
 | ||
| Authority signs revocation bundles and publishes JWKS entries via the new signing manager:
 | ||
| 
 | ||
| - **Configuration (`authority.yaml`):**
 | ||
|   ```yaml
 | ||
|   signing:
 | ||
|     enabled: true
 | ||
|     algorithm: ES256            # Defaults to ES256
 | ||
|     keySource: file             # Loader identifier (file, vault, etc.)
 | ||
|     provider: default           # Optional preferred crypto provider
 | ||
|     activeKeyId: authority-signing-dev
 | ||
|     keyPath: "../certificates/authority-signing-dev.pem"
 | ||
|     additionalKeys:
 | ||
|       - keyId: authority-signing-dev-2024
 | ||
|         path: "../certificates/authority-signing-dev-2024.pem"
 | ||
|         source: "file"
 | ||
|   ```
 | ||
| - **Sources:** The default loader supports PEM files relative to the content root; additional loaders can be registered via `IAuthoritySigningKeySource`.
 | ||
| - **Providers:** Keys are registered against the `ICryptoProviderRegistry`, so alternative implementations (HSM, libsodium) can be plugged in without changing host code.
 | ||
| - **JWKS output:** `GET /jwks` lists every signing key with `status` metadata (`active`, `retired`). Old keys remain until operators remove them from configuration, allowing verification of historical bundles/tokens.
 | ||
| 
 | ||
| ### Rotation SOP (no downtime)
 | ||
| 1. Generate a new P-256 private key (PEM) on an offline workstation and place it where the Authority host can read it (e.g., `../certificates/authority-signing-2025.pem`).
 | ||
| 2. Call the authenticated admin API:
 | ||
|    ```bash
 | ||
|    curl -sS -X POST https://authority.example.com/internal/signing/rotate \
 | ||
|      -H "x-stellaops-bootstrap-key: ${BOOTSTRAP_KEY}" \
 | ||
|      -H "Content-Type: application/json" \
 | ||
|      -d '{
 | ||
|            "keyId": "authority-signing-2025",
 | ||
|            "location": "../certificates/authority-signing-2025.pem",
 | ||
|            "source": "file"
 | ||
|          }'
 | ||
|    ```
 | ||
| 3. Verify the response reports the previous key as retired and fetch `/jwks` to confirm the new `kid` appears with `status: "active"`.
 | ||
| 4. Persist the old key path in `signing.additionalKeys` (the rotation API updates in-memory options; rewrite the YAML to match so restarts remain consistent).
 | ||
| 5. If you prefer automation, trigger the `.gitea/workflows/authority-key-rotation.yml` workflow with the new `keyId`/`keyPath`; it wraps `ops/authority/key-rotation.sh` and reads environment-specific secrets. The older key will be marked `retired` and appended to `signing.additionalKeys`.
 | ||
| 6. Re-run `stella auth revoke export` so revocation bundles are signed with the new key. Downstream caches should refresh JWKS within their configured lifetime (`StellaOpsAuthorityOptions.Signing` + client cache tolerance).
 | ||
| 
 | ||
| The rotation API leverages the same cryptography abstractions as revocation signing; no restart is required and the previous key is marked `retired` but kept available for verification.
 | ||
| 
 | ||
| ## 6. Bootstrap & Administrative Endpoints
 | ||
| Administrative APIs live under `/internal/*` and require the bootstrap API key plus rate-limiter compliance.
 | ||
| 
 | ||
| | Endpoint | Method | Description |
 | ||
| | --- | --- | --- |
 | ||
| | `/internal/users` | `POST` | Provision initial administrative accounts through the registered password-capable plug-in. Emits structured audit events. |
 | ||
| | `/internal/clients` | `POST` | Provision OAuth clients (client credentials / device code). |
 | ||
| | `/internal/revocations/export` | `GET` | Export revocation bundle + detached JWS + digest. |
 | ||
| | `/internal/signing/rotate` | `POST` | Promote a new signing key (see SOP above). Request body accepts `keyId`, `location`, optional `source`, `algorithm`, `provider`, and metadata. |
 | ||
| 
 | ||
| All administrative calls emit `AuthEventRecord` entries enriched with correlation IDs, PII tags, and network metadata for offline SOC ingestion.
 | ||
| 
 | ||
| ## 7. Configuration Reference
 | ||
| 
 | ||
| | Section | Key | Description | Notes |
 | ||
| | --- | --- | --- | --- |
 | ||
| | Root | `issuer` | Absolute HTTPS issuer advertised to clients. | Required. Loopback HTTP allowed only for development. |
 | ||
| | Tokens | `accessTokenLifetime`, `refreshTokenLifetime`, etc. | Lifetimes for each grant (access, refresh, device, authorization code, identity). | Enforced during issuance; persisted on each token document. |
 | ||
| | Storage | `storage.connectionString` | MongoDB connection string. | Required even for tests; offline kits ship snapshots for seeding. |
 | ||
| | Signing | `signing.enabled` | Enable JWKS/revocation signing. | Disable only for development. |
 | ||
| | Signing | `signing.algorithm` | Signing algorithm identifier. | Currently ES256; additional curves can be wired through crypto providers. |
 | ||
| | Signing | `signing.keySource` | Loader identifier (`file`, `vault`, custom). | Determines which `IAuthoritySigningKeySource` resolves keys. |
 | ||
| | Signing | `signing.keyPath` | Relative/absolute path understood by the loader. | Stored as-is; rotation request should keep it in sync with filesystem layout. |
 | ||
| | Signing | `signing.activeKeyId` | Active JWKS / revocation signing key id. | Exposed as `kid` in JWKS and bundles. |
 | ||
| | Signing | `signing.additionalKeys[].keyId` | Retired key identifier retained for verification. | Manager updates this automatically after rotation; keep YAML aligned. |
 | ||
| | Signing | `signing.additionalKeys[].source` | Loader identifier per retired key. | Defaults to `signing.keySource` if omitted. |
 | ||
| | Security | `security.rateLimiting` | Fixed-window limits for `/token`, `/authorize`, `/internal/*`. | See `docs/security/rate-limits.md` for tuning. |
 | ||
| | Bootstrap | `bootstrap.apiKey` | Shared secret required for `/internal/*`. | Only required when `bootstrap.enabled` is true. |
 | ||
| 
 | ||
| ## 8. Offline & Sovereign Operation
 | ||
| - **No outbound dependencies:** Authority only contacts MongoDB and local plugins. Discovery and JWKS are cached by clients with offline tolerances (`AllowOfflineCacheFallback`, `OfflineCacheTolerance`). Operators should mirror these responses for air-gapped use.
 | ||
| - **Structured logging:** Every revocation export, signing rotation, bootstrap action, and token issuance emits structured logs with `traceId`, `client_id`, `subjectId`, and `network.remoteIp` where applicable. Mirror logs to your SIEM to retain audit trails without central connectivity.
 | ||
| - **Determinism:** Sorting rules in token and revocation exports guarantee byte-for-byte identical artefacts given the same datastore state. Hashes and signatures remain stable across machines.
 | ||
| 
 | ||
| ## 9. Operational Checklist
 | ||
| - [ ] Protect the bootstrap API key and disable bootstrap endpoints (`bootstrap.enabled: false`) once initial setup is complete.
 | ||
| - [ ] Schedule `stella auth revoke export` (or `/internal/revocations/export`) at the same cadence as Feedser exports so bundles remain in lockstep.
 | ||
| - [ ] Rotate signing keys before expiration; keep at least one retired key until all cached bundles/tokens signed with it have expired.
 | ||
| - [ ] Monitor `/health` and `/ready` plus rate-limiter metrics to detect plugin outages early.
 | ||
| - [ ] Ensure downstream services cache JWKS and revocation bundles within tolerances; stale caches risk accepting revoked tokens.
 | ||
| 
 | ||
| For plug-in specific requirements, refer to **[Authority Plug-in Developer Guide](dev/31_AUTHORITY_PLUGIN_DEVELOPER_GUIDE.md)**. For revocation bundle validation workflow, see **[Authority Revocation Bundle](security/revocation-bundle.md)**.
 |