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
|
|
@ -6,14 +6,32 @@ from fastapi import Request
|
|||
|
||||
from qonto_assistant.config import Settings
|
||||
from qonto_assistant.contracts import ActorClaims
|
||||
from qonto_assistant.key_cape_auth import KeyCapeAuthError, KeyCapeTokenVerifier
|
||||
|
||||
|
||||
def actor_claims_from_headers(headers: Mapping[str, str], settings: Settings) -> ActorClaims:
|
||||
def actor_claims_from_headers(
|
||||
headers: Mapping[str, str],
|
||||
settings: Settings,
|
||||
*,
|
||||
key_cape_verifier: KeyCapeTokenVerifier | None = None,
|
||||
) -> ActorClaims:
|
||||
"""Shared REST/MCP claims parsing so both transports enforce identical actor identity.
|
||||
|
||||
Real workload/OIDC auth for the MCP transport is QONTO-WP-0003-T03; until
|
||||
then MCP callers use the same X-Actor-* header convention as REST.
|
||||
When a `KeyCapeTokenVerifier` is configured (QONTO-WP-0004-T03), a
|
||||
verified `Authorization: Bearer <jwt>` takes precedence over the
|
||||
self-asserted `X-Actor-*` header convention -- closing the gap
|
||||
`docs/mcp-integration.md` called out explicitly. If the verifier is
|
||||
marked `required`, a missing or invalid bearer token is rejected
|
||||
outright rather than silently falling back to self-asserted headers.
|
||||
"""
|
||||
authorization = headers.get("authorization", "")
|
||||
if authorization.lower().startswith("bearer ") and key_cape_verifier is not None:
|
||||
token = authorization.split(" ", 1)[1].strip()
|
||||
return key_cape_verifier.verify(token)
|
||||
|
||||
if key_cape_verifier is not None and key_cape_verifier.required:
|
||||
raise KeyCapeAuthError("bearer_token_required")
|
||||
|
||||
actor_id = headers.get("x-actor-id", "anonymous")
|
||||
tenant_id = headers.get("x-tenant-id", settings.default_tenant_id)
|
||||
lane = headers.get("x-actor-lane", settings.default_actor_lane)
|
||||
|
|
@ -22,5 +40,10 @@ def actor_claims_from_headers(headers: Mapping[str, str], settings: Settings) ->
|
|||
return ActorClaims(actor_id=actor_id, tenant_id=tenant_id, lane=lane, scopes=scopes)
|
||||
|
||||
|
||||
def actor_claims_from_request(request: Request, settings: Settings) -> ActorClaims:
|
||||
return actor_claims_from_headers(request.headers, settings)
|
||||
def actor_claims_from_request(
|
||||
request: Request,
|
||||
settings: Settings,
|
||||
*,
|
||||
key_cape_verifier: KeyCapeTokenVerifier | None = None,
|
||||
) -> ActorClaims:
|
||||
return actor_claims_from_headers(request.headers, settings, key_cape_verifier=key_cape_verifier)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue