diff --git a/docs/iam-profile-consumption.md b/docs/iam-profile-consumption.md new file mode 100644 index 0000000..4b1c9b2 --- /dev/null +++ b/docs/iam-profile-consumption.md @@ -0,0 +1,164 @@ +# NetKingdom IAM Profile — flex-auth Consumption Surface + +Date: 2026-05-16 +Status: Draft for FLEX-WP-0005 P5.5; binds the input contract for the +standalone evaluator (FLEX-WP-0002) and every PDP adapter (FLEX-WP-0004). +Upstream: `~/the-custodian/canon/standards/iam-profile_v0.1.md`. + +## Boundary + +The NetKingdom IAM Profile defines the OIDC contract shared across hubs +and services. flex-auth **consumes verified claims**; it does not +verify token signatures, fetch JWKS, or terminate OIDC sessions. Those +responsibilities belong upstream: + +- **key-cape (lightweight mode)** validates tokens against its local + OIDC provider and emits claims that conform to the profile. +- **Keycloak (heavy mode)** signs tokens; integration code (e.g. + Markitect's `NetKingdomIdentityClaimsAdapter`) validates issuer, + audience, signature, expiry, and clock skew before handing claims to + flex-auth. + +A flex-auth deployment that exposes a network endpoint MUST be fronted +by an identity layer that does the verification. The flex-auth core +accepts a normalized claim envelope and is responsible for everything +*after* "this caller is authenticated". + +## Input Envelope + +flex-auth's standalone evaluator and adapters consume a normalized +envelope identical to Markitect's `EnterpriseIdentity` shape: + +```yaml +issuer: # required +subject: # required +principal_type: human | service | emergency +audience: [, ...] # required, non-empty +authorized_party: +preferred_username: # required for humans +roles: [, ...] # required, non-empty +scopes: [, ...] # required, non-empty +groups: [, ...] # optional; resolved by directory layer +assurance: + acr: + amr: [, ...] # e.g. pwd, otp, mfa, hwk + mfa: +directory: + groups_claim_present: + group_overage: # Microsoft Entra-style group overage +claims: { ... } # full original claim map (minus 'groups') +provenance: + source: claims | jwt | jwt-fixture + verified_signature: +``` + +This is the envelope every check API call receives, regardless of +which upstream identity provider produced the token. + +## Required Claims (per IAM Profile §"Required Claims") + +flex-auth treats the following as hard requirements. Missing any +produces a `validation_error` before the request reaches a policy +package. + +| Claim | flex-auth field | Notes | +| --- | --- | --- | +| `iss` | `issuer` | Must match the deployment's expected issuer; production rejects local-dev issuers (`localhost`, `127.0.0.1`, `.local`, `dev.local`). | +| `sub` | `subject` | Stable identifier; not a username. | +| `aud` | `audience` | Must include the flex-auth instance or the protected system. | +| `exp` | (validated upstream) | flex-auth tolerates ≤60s clock skew per profile §"Token Lifecycle". | +| `iat` | (validated upstream) | Same. | +| `scope` or `scp` | `scopes` | At least one scope required. Empty scope is a hard fail. | +| `preferred_username` | `preferred_username` | Required for `principal_type=human`. Optional for service accounts. | +| `roles` or `realm_access.roles` or `resource_access..roles` | `roles` | Union of all three sources. At least one role required. | + +## Recommended Claims + +| Claim | flex-auth field | Use | +| --- | --- | --- | +| `email` | `claims.email` | Contact identity; **never** used for authorization decisions. | +| `name` | `claims.name` | Display only. | +| `groups` | `groups` (after resolution) | Authorization input; subject to freshness/overage. | +| `azp` | `authorized_party` | Distinguishes service-account client from impersonating client. | +| `acr` | `assurance.acr` | Authentication context class; gates high-trust scopes. | +| `amr` | `assurance.amr` | Authentication methods; `otp`/`mfa`/`hwk` lift `assurance.mfa` to true. | + +## Tolerated Variations + +flex-auth normalizes — protected systems never see the variation. + +- **Role claim location.** Three OIDC providers ship roles in three + places: top-level `roles`, Keycloak's `realm_access.roles`, and + Keycloak's per-client `resource_access..roles`. flex-auth + unions all three. +- **Scope encoding.** `scope` (space-separated string) and `scp` + (array) both accepted; both produce the same `scopes` array. +- **Audience encoding.** `aud` as a single string or as an array; + flex-auth always normalizes to an array. +- **MFA signal.** Either an explicit `mfa: true` claim or any of + `otp`/`mfa`/`hwk` in `amr` produces `assurance.mfa = true`. + +## Principal-Type Detection + +flex-auth classifies the principal by: + +1. If `client_id` is set and `service` is in `roles` → `service`. +2. If `azp` starts with `svc-` or `service` is in `roles` → `service`. +3. If `emergency` is in `roles` → `emergency`. +4. Otherwise → `human`. + +This matches Markitect's `NetKingdomIdentityClaimsAdapter._principal_type` +and follows IAM Profile §"Hub-to-Hub Service Account Pattern" (service +accounts named `svc-*` and carrying the `service` role). + +## Group Overage and Freshness + +Microsoft Entra and Keycloak both clip the `groups` claim once a +threshold is reached; the token then carries `hasgroups: true` (Entra) +or `_claim_names.groups` (also Entra). flex-auth's directory layer is +responsible for resolving the full set via Graph/SCIM/Keycloak admin +API; the claim envelope carries `directory.group_overage = true` so +policy packages can decide whether to fail-closed or accept the +partial set with an `audit_only` outcome. + +Group freshness is tracked at the directory-resolver layer (out of +scope for this document; see FLEX-WP-0004 T05). + +## Production vs Local Development + +Per IAM Profile §"Local Development Profile": + +- Local-development issuers (`localhost`, `127.0.0.1`, hostnames + ending in `.local`, `dev.local`) are rejected when + `environment=production` is set in the request context. +- A development token marked clearly through issuer/audience is + accepted in non-production environments. +- The local-development path exists to keep flex-auth useful before + Keycloak is wired in; it never weakens production rules. + +## Emergency Principals + +Per IAM Profile §"Human Override and Emergency Access": + +- `emergency` is a first-class `principal_type`. +- Every decision involving an emergency principal MUST record a + `record_emergency` obligation in the decision envelope. +- Policy packages MAY allow emergency principals; flex-auth's audit + layer ensures the action is durable regardless. + +## Reference Implementation + +Markitect's `NetKingdomIdentityClaimsAdapter` (at +`markitect-tool/src/markitect_tool/policy/enterprise.py`) implements +the validation steps above in Python. flex-auth's Go implementation +(FLEX-WP-0002 P2.4) mirrors its behavior and stays in sync via +contract tests against the fixtures in `examples/claims/`. + +## Open Items + +- Whether `roles` becomes canonical and `realm_access.roles` becomes + legacy is still listed as an open question in IAM Profile v0.1. As + of 2026-05-16 flex-auth normalizes both with no preference. +- `Workload identity` (Kubernetes service-account tokens, GCP/AWS + workload-identity federation) is not yet in the IAM Profile. + flex-auth's service-account handling is currently OIDC-only. diff --git a/examples/claims/README.md b/examples/claims/README.md new file mode 100644 index 0000000..c0b020a --- /dev/null +++ b/examples/claims/README.md @@ -0,0 +1,23 @@ +# examples/claims/ + +Contract fixtures for the NetKingdom IAM Profile v0.1 claim shapes +flex-auth must accept. Each file is the *raw verified claim map* as +flex-auth receives it from the upstream identity layer (key-cape or +Keycloak); flex-auth's normalization produces the same +`EnterpriseIdentity`-shaped envelope for all of them. + +See `docs/iam-profile-consumption.md` for the full consumption +surface. + +| Fixture | Provider | Demonstrates | +| --- | --- | --- | +| `key-cape-lightweight.yaml` | key-cape lightweight mode | Profile-conformant minimum: single audience, top-level `roles` array, single-factor `amr=pwd`. | +| `keycloak-heavy.yaml` | Keycloak production | Full variation set: `realm_access.roles` + `resource_access..roles`, scope as space-separated string, MFA via `amr=otp`, multiple audiences. | +| `service-account.yaml` | Either provider | Hub-to-hub service account; `service` + `operator` roles, no `preferred_username`, narrow scope. | +| `emergency.yaml` | Either provider | Break-glass human identity; `emergency` role, short expiry, hardware MFA, audit-trail metadata in an `emergency` claim. | +| `keycloak-group-overage.yaml` | Entra/Keycloak | Group-claim overage signal (`hasgroups: true`); flex-auth's directory resolver fetches the full set. | + +These fixtures are loaded by the standalone evaluator's contract tests +(`FLEX-WP-0002 P2.4`) and by the Topaz adapter's contract tests +(`FLEX-WP-0004 T01`). Both code paths MUST produce identical +normalized envelopes for the same fixture. diff --git a/examples/claims/emergency.yaml b/examples/claims/emergency.yaml new file mode 100644 index 0000000..24280f6 --- /dev/null +++ b/examples/claims/emergency.yaml @@ -0,0 +1,31 @@ +# Claim envelope for an emergency (break-glass) human principal. Short +# expiry, emergency role, requires MFA per the profile, and triggers +# durable audit recording on every flex-auth decision that involves it. +# +# Reference: NetKingdom IAM Profile v0.1 §"Human Override and Emergency +# Access". flex-auth maps this to principal_type=emergency and emits a +# `record_emergency` obligation on every decision. + +iss: https://sso.netkingdom.example/realms/netkingdom +sub: f1c4f64e-2c0c-4cda-8c9f-9f3f8f3a2b0e +aud: + - flex-auth +exp: 1767226200 # iat + 10 minutes; emergency tokens are short-lived +iat: 1767225600 +auth_time: 1767225595 +azp: ops-console +preferred_username: ada +email: ada@netkingdom.example +scope: openid profile hub:admin +roles: + - emergency + - admin +amr: + - pwd + - otp + - hwk +acr: "3" +emergency: + incident_id: INC-2026-0042 + authorized_by: "team:platform-stewards" + reason: "credential rotation playbook step 4" diff --git a/examples/claims/key-cape-lightweight.yaml b/examples/claims/key-cape-lightweight.yaml new file mode 100644 index 0000000..4abf963 --- /dev/null +++ b/examples/claims/key-cape-lightweight.yaml @@ -0,0 +1,24 @@ +# Claim envelope a key-cape (lightweight mode) deployment emits for an +# authenticated human user. Profile-conformant minimum: required claims +# only, single audience, simple roles list, OIDC standard amr values. +# +# Reference: docs/iam-profile-consumption.md, NetKingdom IAM Profile v0.1 +# §"Required Claims" and §"Local Development Profile". + +iss: https://idp.netkingdom.local/keycape +sub: user-7f9e2b +aud: + - flex-auth +exp: 4102444800 # 2100-01-01, kept far-future for stable fixtures +iat: 1767225600 # 2026-01-01 +preferred_username: ada +email: ada@netkingdom.local +name: Ada Lovelace +scope: openid profile hub:read +roles: + - viewer +amr: + - pwd +acr: "1" +groups: + - /markitect/readers diff --git a/examples/claims/keycloak-group-overage.yaml b/examples/claims/keycloak-group-overage.yaml new file mode 100644 index 0000000..3294cd3 --- /dev/null +++ b/examples/claims/keycloak-group-overage.yaml @@ -0,0 +1,26 @@ +# Claim envelope when the token-side `groups` list has been clipped by +# the IdP. Both Microsoft Entra and Keycloak signal this differently; +# this fixture shows the Entra-style `hasgroups: true` flag. flex-auth +# sets directory.group_overage = true and depends on the directory +# resolver (FLEX-WP-0004 T05) to fetch the full set. +# +# Reference: docs/iam-profile-consumption.md §"Group Overage and +# Freshness". + +iss: https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/v2.0 +sub: f1c4f64e-2c0c-4cda-8c9f-9f3f8f3a2b0e +aud: + - flex-auth +exp: 4102444800 +iat: 1767225600 +preferred_username: ada +name: Ada Lovelace +scope: openid profile hub:read +roles: + - viewer +hasgroups: true +_claim_names: + groups: src1 +_claim_sources: + src1: + endpoint: https://graph.microsoft.com/v1.0/users/f1c4f64e/getMemberObjects diff --git a/examples/claims/keycloak-heavy.yaml b/examples/claims/keycloak-heavy.yaml new file mode 100644 index 0000000..1e3cdeb --- /dev/null +++ b/examples/claims/keycloak-heavy.yaml @@ -0,0 +1,43 @@ +# Claim envelope a Keycloak (heavy mode) deployment emits for an +# authenticated human user with MFA. Demonstrates the full set of +# variations flex-auth must normalize: roles in realm_access AND +# resource_access, scope as space-separated string, multiple audiences, +# enriched assurance via amr=otp. +# +# Reference: docs/iam-profile-consumption.md §"Tolerated Variations". + +iss: https://sso.netkingdom.example/realms/netkingdom +sub: f1c4f64e-2c0c-4cda-8c9f-9f3f8f3a2b0e +aud: + - flex-auth + - markitect-tool +exp: 4102444800 +iat: 1767225600 +auth_time: 1767225590 +azp: markitect-cli +preferred_username: ada +email: ada@netkingdom.example +email_verified: true +name: Ada Lovelace +given_name: Ada +family_name: Lovelace +scope: openid profile email hub:read hub:write hub:capability +realm_access: + roles: + - default-roles-netkingdom + - operator +resource_access: + flex-auth: + roles: + - reader + markitect-tool: + roles: + - editor +groups: + - /platform/architecture + - /markitect/readers +amr: + - pwd + - otp +acr: "2" +sid: 4c0a3a8a-3a47-4f2f-8e89-9e5f9b0a0a0a diff --git a/examples/claims/service-account.yaml b/examples/claims/service-account.yaml new file mode 100644 index 0000000..6abb0e1 --- /dev/null +++ b/examples/claims/service-account.yaml @@ -0,0 +1,20 @@ +# Claim envelope for a hub-to-hub service account (client_credentials +# grant). Profile-required `service` role, scoped tightly to the +# operation it performs. No preferred_username (service identities are +# named after the service and environment per the profile). +# +# Reference: NetKingdom IAM Profile v0.1 §"Service Account Flow" and +# §"Hub-to-Hub Service Account Pattern". + +iss: https://sso.netkingdom.example/realms/netkingdom +sub: svc-markitect-tool-prod +aud: + - flex-auth +exp: 4102444800 +iat: 1767225600 +azp: svc-markitect-tool-prod +client_id: svc-markitect-tool-prod +scope: hub:read hub:capability +roles: + - service + - operator diff --git a/workplans/FLEX-WP-0005-foundations-and-topaz-alignment.md b/workplans/FLEX-WP-0005-foundations-and-topaz-alignment.md index 8bbe70e..e69ed0a 100644 --- a/workplans/FLEX-WP-0005-foundations-and-topaz-alignment.md +++ b/workplans/FLEX-WP-0005-foundations-and-topaz-alignment.md @@ -3,7 +3,7 @@ id: FLEX-WP-0005 type: workplan title: "Foundations and Topaz Alignment" domain: netkingdom -status: todo +status: done owner: flex-auth topic_slug: flex-auth planning_priority: P0 @@ -154,7 +154,7 @@ both `FLEX-WP-0002 P2.1` and `FLEX-WP-0004 T001`. ```task id: FLEX-WP-0005-T005 -status: todo +status: done priority: medium state_hub_task_id: "b31dab7b-e72c-4abe-b6d5-f5875fd0c25a" ```