From 4c58a4414bcfb65e28e349a6d5327ca40532413a93daaacae5213d16266b9f62 Mon Sep 17 00:00:00 2001 From: Tyler J King Date: Wed, 1 Apr 2026 18:24:18 -0400 Subject: [PATCH] feat: JWT token extraction for Keycloak driver + deploy fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added _extract_token_data() to authorize endpoint. Extracts JWT from Authorization header and passes it to the Keycloak identity driver as _token_data. This was the missing link — the driver needs the token to resolve the principal DID. Verified on Hetzner: AC issued for tyler@bxnet.io → did:web:bxnet.capstone.guildhouse.dev/principal/tyler@bxnet.io Chronicle event emitted (GSAP_AC_ISSUED) Known issue: CR endpoint has SQLAlchemy async greenlet bug (MissingGreenlet on the select+update in complete handler). AC issuance works. CR needs async session fix. Co-Authored-By: Claude Opus 4.6 (1M context) --- gsap_broker/routers/authorize.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/gsap_broker/routers/authorize.py b/gsap_broker/routers/authorize.py index a841579..8906060 100644 --- a/gsap_broker/routers/authorize.py +++ b/gsap_broker/routers/authorize.py @@ -1,7 +1,7 @@ """POST /governance/authorize/ — GSAP §5.2""" import secrets, uuid from datetime import datetime, timedelta, UTC -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Request from sqlmodel.ext.asyncio.session import AsyncSession from gsap_broker.db import get_session from gsap_broker.db_models import AuthorizationContextDB @@ -14,14 +14,33 @@ from gsap_broker import chronicle router = APIRouter() + +def _extract_token_data(http_request: Request) -> dict: + """Extract and decode JWT from Authorization header (unverified — driver validates).""" + auth = http_request.headers.get("authorization", "") + if not auth.startswith("Bearer "): + return {} + token = auth[7:] + try: + import base64, json + payload = token.split(".")[1] + payload += "=" * (4 - len(payload) % 4) + return json.loads(base64.urlsafe_b64decode(payload)) + except Exception: + return {} + + @router.post("/authorize/", response_model=AuthorizeResponse, summary="Issue AC (GSAP §5.2)") -async def authorize(request: AuthorizeRequest, db: AsyncSession = Depends(get_session)): +async def authorize(body: AuthorizeRequest, http_request: Request, db: AsyncSession = Depends(get_session)): + request = body + token_data = _extract_token_data(http_request) try: driver = DriverRegistry.get(request.driver_id, config={ "requested_accord": request.accord_template, "domain": settings.keycloak_domain, "did_template": settings.keycloak_did_template, "elevated_suffix": settings.keycloak_elevated_role_suffix, + "_token_data": token_data, }) except KeyError as e: raise HTTPException(status_code=400, detail=str(e))