2026-03-17 23:10:44 +01:00
# SCOPE
2026-09-05 01:27:16 +02:00
Reviewed 2026-09-05 against source revision `b989de4` .
## Purpose and boundary
KeyCape is Go identity **Tooling** for NetKingdom's lightweight IAM deployment.
It runs its own OIDC-style issuer, uses Authelia for upstream browser
authentication, reads identities from LLDAP, and delegates factor checks to
privacyIDEA. It also provides caller authentication commands, directory snapshot
validation, and migration artifact generators.
It issues identity, role, scope and assurance claims. Resource authorization
belongs to access-engine and consuming services; secret custody and OpenBao
policy/token enforcement belong to their platform owners. The implemented subset
supports NetKingdom integrations, but complete profile conformance and drop-in
Keycloak interchangeability are not established.
## Implemented capabilities
| Surface | What the current source provides |
| --- | --- |
| Issuer HTTP API | Discovery, `/authorize` and its callback/registration-return routes, `/token` , `/jwks` , `/userinfo` , local `/logout` , and `/healthz` . |
| Human authentication | Authorization code with S256 PKCE, exact registered redirects, scope allow-lists, Authelia login, privacyIDEA MFA challenges, client-specific assurance requirements, browser session reuse, step-up/fresh-login handling, and configured registration/enrollment handoffs. |
| Service authentication | Static confidential `client_credentials` clients authenticated with form-encoded `client_secret_basic` ; configured subject, tenant, roles, scopes and per-client token lifetime. Secrets are resolved from environment references at startup. |
| Tokens and identity | Locally signed RS256 JWTs; configurable access-token resource audience while ID tokens retain the client audience; human/service principal types, tenant, groups, roles, scope and assurance claims. UserInfo resolves canonical directory subjects and filters profile/email/groups by scope. |
| Caller commands | `keycape login` for public-client browser PKCE login and `keycape service-token` for service exchange. HTTPS discovery/JWKS verification and private JSON token-file delivery outside Git; no token output on stdout. |
2026-09-07 08:45:50 +02:00
| Validation and migration | Canonical snapshot checks; deterministic LLDAP user/group/membership export that records whether it enumerated the whole directory; basic Keycloak realm JSON; LDIF generation for OpenLDAP, 389 Directory Server and AD targets. These generate artifacts rather than execute a complete migration. |
2026-09-05 01:27:16 +02:00
| Diagnostics and packaging | Structured authentication/enforcement/migration events, a process health response, Go build/test/vet targets, a container containing the KeyCape binary, and development/CI scaffolding. |
## Material limits
- Client registrations, service secrets and the signing key are loaded at startup.
Login, authorization and handoff state are process-local. There is no shared
session store, general refresh-token flow, token introspection/revocation API,
or automatic signing-key/client-secret rotation service. Logout clears the
local KeyCape session, not every upstream or downstream session/token.
- The optional tenant-engine `tenant_roles` adapter and handler support exist,
but the server entry point does not configure them. That claim is not an
enabled capability of the stock executable.
2026-09-07 08:45:50 +02:00
- The LLDAP export enumerates the group subtree directly, so groups with no
members are present, and every snapshot carries a `groupEnumeration` field
saying whether that enumeration ran or the membership-derived fallback did
(KEY-WP-0018). A failed enumeration aborts the export rather than writing a
smaller snapshot, and read failures are reported rather than skipped. Users,
groups and memberships are sorted, so an unchanged directory exports
identically. Completeness beyond users, groups and memberships — passwords and
MFA credentials in particular — is still not covered.
2026-09-05 01:27:16 +02:00
- The Keycloak CLI exports users/groups through the basic transformer and has
no client-list input. Library-level client mapping does not preserve the full
current service-identity, audience, tenant/role, MFA and lifetime contract.
Password and MFA credential migration is not supplied.
- Snapshot validation is a limited Go rule set, not full machine-readable schema
Reconcile the canonical model and discovery with the runtime
Closes gap G02 of the scope assessment for the client-registration and discovery
surface. spec/canonical-model.yaml and domain/model.go both claimed to be the
source of truth and disagreed: the spec restricted grants to authorization_code,
required redirect URIs of every client, and omitted the audience, service
subject, tenant, role, MFA and handoff fields the runtime reads.
The durable part is the link, not the edit. A two-way conformance test compares
the spec against the Go model by reflection and fails when a runtime field has
no spec entry or a spec entry is not read by the runtime, the latter unless
marked runtime: false. It found drift beyond the assessment's list on its first
run -- User.tenant was undeclared -- which is the argument for the check over a
one-time reconciliation.
Discovery now advertises the core profile claims that appear on every token and
derives scopes_supported from the registered clients rather than a fixed list.
The Go model is stated as the runtime authority and the YAML as the reviewed
contract, in both files. This covers client registration and discovery, not
schema enforcement in general, which remains G06.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-07 00:22:49 +02:00
enforcement. The canonical YAML model and discovery metadata now match the
runtime client-registration surface and are held there by a conformance check
(KEY-WP-0017); the Go model is the runtime authority and the YAML the reviewed
contract. That is narrower than schema enforcement in general.
- The authorization-code grant now binds the redirect URI,
Harden the authorization-code grant and UserInfo verification
Closes the local protocol surface of gap G01 from the scope assessment
(KEY-WP-0016). The browser grant validated PKCE, client id and scopes but left
four bindings unenforced, and UserInfo verified less than the caller CLI does.
Authorization-code path: bind the exchange to the redirect URI the code was
issued for, refuse clients whose registration does not permit the grant, and
authenticate confidential clients with a digest-based constant-time comparison
over the same credential sources as the service grant. An empty grantTypes stays
an implicit authorization-code client, matching config validation.
Code consumption: SessionStore.Consume reads and deletes under one lock. The
previous Get/Delete pair spanned JWT signing, and the added test reproduces the
race against that version -- 9 of 16 concurrent exchanges succeeded, and a
failed exchange left the code replayable.
UserInfo: check the JOSE header algorithm before trusting the signature, require
the configured issuer, and require an access token rather than accepting an ID
token of the right shape. Purpose is decided on the scope claim so the issued
token contract, which consumers pin exactly, does not change.
SCOPE.md and the assessment record which bindings are now enforced and that the
Authelia upstream-trust assumption remains open, so G01 is not fully closed and
no profile-conformance claim is made.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-06 22:43:47 +02:00
enforces grant-type eligibility, authenticates confidential clients and
consumes codes atomically, and UserInfo enforces algorithm, issuer and
Verify upstream Authelia ID tokens
Closes the remaining half of gap G01. The adapter decoded upstream ID-token
claims without verifying anything, justified in a comment by a server-to-server
TLS boundary that nothing enforced.
Operator decision: verify the token rather than police the transport. The hop is
to be HTTPS as defence in depth, but KeyCape does not monitor, check or gate on
that -- a transport check helps only when it is configured correctly, which is
the assumption it was meant to remove. Verification holds regardless of how the
token arrived, so no HTTPS validation or opt-in flag is added.
HandleCallback now verifies the RS256 signature against Authelia's published
keys, the issuer Authelia advertises, KeyCape's own client ID in the audience,
and a sane validity window, before any claim is trusted. It fails closed: an
unreachable or unparseable key set denies the login. The advertised jwks_uri
path is rebased onto the server-side token base URL so split-horizon deployments
resolve, with config overrides where that inference is wrong, and an unknown key
id triggers one refresh so provider rotation needs no restart.
The reusable half lives in internal/jose rather than being copied from
authclient's verifier, since duplicated verification is how two copies drift and
one misses a fix. Migrating authclient onto it is tracked as KEY-WP-0019-T05,
kept separate so it does not destabilise a tested path in this change.
Thirteen rejection cases plus algorithm and rotation coverage; with the
unverified parse restored all fifteen fail, so they test the fix rather than
merely passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-07 08:51:42 +02:00
access-token purpose (KEY-WP-0016). Upstream Authelia ID tokens are verified
before any claim is trusted — signature against the provider's published keys,
advertised issuer, own client ID in the audience, validity window — failing
closed and independently of transport (KEY-WP-0019). KeyCape deliberately does
not check or gate on the upstream transport. Enforcing these bindings
2026-09-07 00:17:56 +02:00
is not complete profile conformance. Relying parties must repeat `redirect_uri`
on the token exchange and present the access token, not the ID token, to
`/userinfo` ; see [authorization-code bindings ](docs/authorization-code-bindings.md ).
See the assessment below.
2026-09-05 01:27:16 +02:00
- Tests cover local handlers, adapters, transformations and CLI protocol behavior.
They do not establish complete replacement against a running Keycloak/full-LDAP
stack. The Scenario B/C shell harnesses are incomplete.
- The server listens on HTTP; HTTPS termination is deployment-owned.
`/healthz` reports process status without probing dependencies. Development
Compose needs configuration/key material absent from the checkout. Production
deployment and custody are external; a source implementation or example client
fragment does not prove live registration or consumer cutover.
- Approval-client provisioning, coordinated Qonto rotation, native consumer
handoffs and notification receipt closure remain open in KEY-WP-0009/0013/0014.
## Deliberate exclusions
General-purpose IAM, dynamic client registration, implicit/password grants,
wildcard redirects, arbitrary identity brokering, resource authorization policy,
OpenBao credential custody, and Keycloak operations are outside this repository's
ownership. Registration/enrollment handoffs do not implement user provisioning
or factor enrollment themselves.
## Entry points and verification
Run the issuer with `keycape --config PATH` ; `keycape server` and
`keycape migrate` are not implemented subcommands. Caller commands are
`keycape login` and `keycape service-token` .
Separate binaries under `src/cmd/` are `validator` , `lldap-export` ,
`keycape-to-keycloak` , and `lldap-to-ldap` . `make -C src build` places binaries in
root `bin/` ; the container packages only `keycape` . Tests live under `src/tests/`
and alongside packages in `src/internal/` .
`make test` , `make lint` (Go vet), `make build` , and `make contract-test` are the
root checks. The capability-contract check requires the sibling NetKingdom
validator. These checks passed for the reviewed revision in the preceding
implementation session; the scope assessment is a source/documentation review.
Use this repo for its bounded issuer, caller JWT acquisition, identity adapters
and migration preparation. Consult [INTENT.md ](INTENT.md ) for the architectural
target, [native authentication ](docs/native-authentication.md ) for caller
constraints, and the [scope/intent assessment ](history/2026-09-05-011726-scope-intent-assessment.md )
for evidence, priorities and remaining gaps.
## Provided capabilities
2026-03-19 21:41:24 +01:00
```capability
type: security
2026-09-05 01:27:16 +02:00
title: Bounded OIDC and service-token issuance
description: Implements static-client browser PKCE and client-credentials authentication with RS256 identity claims through Authelia, LLDAP and privacyIDEA; complete Keycloak interchangeability remains unproven.
keywords: [oidc, pkce, authentication, iam, sso, mfa, identity, service-token]
2026-03-19 21:41:24 +01:00
```
```capability
type: security
2026-09-05 01:27:16 +02:00
title: Verified caller JWT acquisition
description: Provides native browser login and service-token exchange commands with HTTPS discovery, JWKS verification and private token-file delivery.
keywords: [oidc, pkce, jwt, cli, authentication, jwks]
2026-03-19 21:41:24 +01:00
```
2026-09-05 01:27:16 +02:00
```capability
type: security
title: Directory validation and migration artifact generation
description: Checks canonical snapshots and produces LLDAP exports, basic Keycloak realm JSON and target-specific LDIF; does not perform or prove a complete live migration.
keywords: [migration, identity, lldap, keycloak, ldif, validation]
```