feat: JWT token extraction for Keycloak driver + deploy fix

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) <noreply@anthropic.com>
This commit is contained in:
Tyler J King 2026-04-01 18:24:18 -04:00
parent 0c77943ceb
commit 4c58a4414b

View file

@ -1,7 +1,7 @@
"""POST /governance/authorize/ — GSAP §5.2""" """POST /governance/authorize/ — GSAP §5.2"""
import secrets, uuid import secrets, uuid
from datetime import datetime, timedelta, UTC 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 sqlmodel.ext.asyncio.session import AsyncSession
from gsap_broker.db import get_session from gsap_broker.db import get_session
from gsap_broker.db_models import AuthorizationContextDB from gsap_broker.db_models import AuthorizationContextDB
@ -14,14 +14,33 @@ from gsap_broker import chronicle
router = APIRouter() 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)") @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: try:
driver = DriverRegistry.get(request.driver_id, config={ driver = DriverRegistry.get(request.driver_id, config={
"requested_accord": request.accord_template, "requested_accord": request.accord_template,
"domain": settings.keycloak_domain, "domain": settings.keycloak_domain,
"did_template": settings.keycloak_did_template, "did_template": settings.keycloak_did_template,
"elevated_suffix": settings.keycloak_elevated_role_suffix, "elevated_suffix": settings.keycloak_elevated_role_suffix,
"_token_data": token_data,
}) })
except KeyError as e: except KeyError as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))