QONTO-WP-0004-T03: verify key-cape IAM Profile tokens
Replaces the interim shared-secret bearer token's role as the identity
boundary with real key-cape JWKS-based verification, closing the gap
docs/mcp-integration.md called out explicitly ("no OIDC issuer exists
in this fleet yet") -- key-cape's /jwks is a standard RS256 endpoint
and needed no key-cape-side work to consume.
KeyCapeTokenVerifier fetches and caches signing keys over httpx
(matching FlexAuthCheckClient's pattern elsewhere in this codebase),
validates iss/aud/exp and the IAM Profile v0.3 required claims, and
derives ActorClaims from the token (tenant, scopes, and a lane
inferred from the roles claim). Wired into auth.actor_claims_from_headers,
the single seam both REST and MCP already used -- a verified bearer
token now takes precedence over self-asserted X-Actor-* headers, and
can be made mandatory via QONTO_KEY_CAPE_REQUIRED once real tokens are
issued to callers. Off by default (no QONTO_KEY_CAPE_JWKS_URL set) so
existing deployments are unaffected until configured.
The QONTO_ASSISTANT_MCP_TOKEN shared secret remains as a documented
local-dev/legacy fallback, not the auth boundary going forward.
Verified: 13 new tests (test_key_cape_auth.py) covering valid/expired/
wrong-audience/wrong-issuer/missing-claim/unknown-key/rotated-key
tokens plus the auth.py precedence and required-vs-optional
integration paths, using a real generated RSA keypair and JWKS served
over httpx.MockTransport. Full suite -> 52 passed; REST and MCP smokes
both still pass against fixtures; compileall clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
21c98fc062
commit
aa28ef353a
9 changed files with 514 additions and 33 deletions
|
|
@ -22,18 +22,26 @@ Two independent layers, same as the blueprint's identity separation
|
|||
(`specs/ArchitectureBlueprint.md` §4.7):
|
||||
|
||||
1. **Workload auth (is this caller allowed to reach the service at all?)**
|
||||
Today: a static shared-secret bearer token
|
||||
(`QONTO_ASSISTANT_MCP_TOKEN`), checked by `BearerTokenAuthMiddleware`
|
||||
in front of `/mcp`. If the env var is unset, the middleware is not
|
||||
installed — used only for local fixture-backed smoke work, never for a
|
||||
deployment holding real credentials.
|
||||
Two mechanisms now coexist:
|
||||
|
||||
This is **not** the OIDC/workload-identity target the blueprint
|
||||
describes — there is no OIDC issuer in this fleet yet to federate
|
||||
against. A shared-secret bearer token is the deployable "or similar"
|
||||
primitive for now; upgrading to real OIDC/workload identity is
|
||||
fleet-level follow-on work (tracked as a Phase 2 gap, not closed by this
|
||||
task).
|
||||
- **key-cape IAM Profile tokens (QONTO-WP-0004-T03, current target).**
|
||||
Set `QONTO_KEY_CAPE_JWKS_URL` (and, if not using the defaults,
|
||||
`QONTO_KEY_CAPE_ISSUER`/`QONTO_KEY_CAPE_AUDIENCE`) and a
|
||||
`KeyCapeTokenVerifier` is built and wired into both REST and MCP
|
||||
(`auth.actor_claims_from_headers`). A verified `Authorization: Bearer
|
||||
<jwt>` takes precedence over self-asserted headers; actor identity
|
||||
(tenant, scopes, and a lane derived from the profile's `roles` claim)
|
||||
comes from the verified token, not the caller's assertion. Set
|
||||
`QONTO_KEY_CAPE_REQUIRED=true` to reject any request without a valid
|
||||
bearer token outright, once real key-cape tokens are actually being
|
||||
issued to callers — until then, leave it `false` so unconfigured
|
||||
deployments keep working during rollout.
|
||||
- **Shared-secret bearer token (legacy/local-dev).**
|
||||
`QONTO_ASSISTANT_MCP_TOKEN`, checked by `BearerTokenAuthMiddleware` in
|
||||
front of `/mcp` only (REST has no equivalent — see below). If unset,
|
||||
the middleware is not installed. This predates the key-cape
|
||||
integration and should be treated as fixture/local-dev only, never the
|
||||
auth boundary for a deployment holding real credentials.
|
||||
|
||||
This token is a **service credential**, not a bank credential — it never
|
||||
reaches Qonto and is never logged. Treat it like any other shared
|
||||
|
|
@ -41,11 +49,15 @@ Two independent layers, same as the blueprint's identity separation
|
|||
plaintext config committed to a repo.
|
||||
|
||||
2. **Actor identity (who is calling, for policy and audit purposes?)**
|
||||
Same `X-Actor-ID` / `X-Tenant-ID` / `X-Actor-Lane` / `X-Actor-Scopes`
|
||||
header convention REST already uses (`auth.actor_claims_from_headers`).
|
||||
These are self-asserted today, not cryptographically bound to the bearer
|
||||
token — tightening that binding is exactly what Phase 3's flex-auth
|
||||
resource (`finance.qonto.read`) is for.
|
||||
When a key-cape verifier is configured, actor identity comes from the
|
||||
verified token's claims (`sub` → actor id, `tenant` → tenant id, `roles`
|
||||
→ lane, `scope`/`scp` → scopes) — see
|
||||
`src/qonto_assistant/key_cape_auth.py`. Otherwise, the same `X-Actor-ID`
|
||||
/ `X-Tenant-ID` / `X-Actor-Lane` / `X-Actor-Scopes` header convention
|
||||
REST and MCP both use (`auth.actor_claims_from_headers`) applies,
|
||||
self-asserted and not cryptographically bound to anything — the gap
|
||||
that configuring key-cape closes. Tightening this further with a live
|
||||
`flex-auth` decision on `finance.qonto.read` is QONTO-WP-0004-T04.
|
||||
|
||||
## One shared client config snippet
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue