Skip to main content

Install

pip install grantex-fastapi

Quick Start

One dependency protects your entire API:
from fastapi import Depends, FastAPI
from grantex import VerifiedGrant
from grantex_fastapi import GrantexAuth, GrantexFastAPIError, grantex_exception_handler

app = FastAPI()
app.add_exception_handler(GrantexFastAPIError, grantex_exception_handler)

JWKS_URI = "https://grantex-auth-dd4mtrt2gq-uc.a.run.app/.well-known/jwks.json"
grantex = GrantexAuth(jwks_uri=JWKS_URI)

@app.get("/api/calendar")
async def calendar(grant: VerifiedGrant = Depends(grantex.scopes("calendar:read"))):
    return {"principalId": grant.principal_id, "scopes": list(grant.scopes)}
After verification succeeds, grant is a fully typed VerifiedGrant dataclass with the principal, agent, scopes, and timestamps.

How It Works

Client → Authorization: Bearer <grantToken> → FastAPI app

                                         Depends(grantex)
                                            │         │
                                       Valid JWT?   Invalid/Missing?
                                            │         │
                                   Return VerifiedGrant   Raise GrantexFastAPIError
                                            │                    │
                                      .scopes() check      grantex_exception_handler
                                       │           │              │
                                  Has scopes?   Missing?     JSON error response
                                       │           │
                                  Return grant   Raise 403
  1. GrantexAuth extracts the Bearer token from the Authorization header
  2. It verifies the RS256 signature against the Grantex JWKS endpoint
  3. On success, a VerifiedGrant is returned and injected into your handler
  4. .scopes() optionally checks that the grant contains all required scopes
  5. Errors are converted to JSON responses by grantex_exception_handler

Token Verification Only

Use Depends(grantex) to verify the token without checking scopes:
@app.get("/api/me")
async def me(grant: VerifiedGrant = Depends(grantex)):
    return {"principalId": grant.principal_id, "agentDid": grant.agent_did}

Scope Enforcement

Use Depends(grantex.scopes(...)) to verify the token AND check scopes in one step:
# Single scope
@app.get("/api/calendar")
async def calendar(grant: VerifiedGrant = Depends(grantex.scopes("calendar:read"))):
    return {"events": get_calendar_events(grant.principal_id)}

# Multiple scopes — all required
@app.post("/api/email/send")
async def send_email(grant: VerifiedGrant = Depends(grantex.scopes("email:read", "email:send"))):
    return {"sent": True}

Via function call

Check scopes inside the route handler with require_scopes():
from grantex_fastapi import require_scopes

@app.get("/api/data")
async def data(grant: VerifiedGrant = Depends(grantex)):
    require_scopes(grant, "data:read")
    return {"data": get_data(grant.principal_id)}

Custom Token Extraction

By default, the middleware reads the Authorization: Bearer <token> header. Override this with token_extractor:
from fastapi import Request

def extract_from_cookie(request: Request) -> str | None:
    return request.cookies.get("grant_token")

grantex = GrantexAuth(jwks_uri=JWKS_URI, token_extractor=extract_from_cookie)

Error Handling

Register the built-in exception handler to return JSON error responses:
app.add_exception_handler(GrantexFastAPIError, grantex_exception_handler)
Or write a custom handler:
from fastapi import Request
from fastapi.responses import JSONResponse
from grantex_fastapi import GrantexFastAPIError

@app.exception_handler(GrantexFastAPIError)
async def custom_handler(request: Request, exc: GrantexFastAPIError) -> JSONResponse:
    if exc.code == "TOKEN_EXPIRED":
        return JSONResponse(
            status_code=401,
            content={"error": "session_expired", "message": "Please re-authorize."},
        )
    return JSONResponse(
        status_code=exc.status_code,
        content={"error": exc.code, "message": str(exc)},
    )

Error Codes

CodeHTTP StatusWhen
TOKEN_MISSING401No token found in the request
TOKEN_INVALID401JWT signature or format is invalid
TOKEN_EXPIRED401JWT exp claim is in the past
SCOPE_INSUFFICIENT403Token is missing one or more required scopes

grant Reference

The VerifiedGrant dataclass contains:
FieldTypeDescription
token_idstrJWT jti claim — unique token identifier
grant_idstrGrantex grant record ID
principal_idstrEnd-user who authorized the agent
agent_didstrAgent’s decentralized identifier
developer_idstrDeveloper organization ID
scopestuple[str, ...]Scopes the agent was granted
issued_atintToken issued-at (seconds since epoch)
expires_atintToken expiry (seconds since epoch)
parent_agent_didstr | NoneParent agent DID (delegated grants only)
parent_grant_idstr | NoneParent grant ID (delegated grants only)
delegation_depthint | NoneDelegation depth (0 = root grant)

Full Example

A complete FastAPI application protected by Grantex:
from fastapi import Depends, FastAPI
from grantex import VerifiedGrant
from grantex_fastapi import GrantexAuth, GrantexFastAPIError, grantex_exception_handler

app = FastAPI(title="My Agent API")
app.add_exception_handler(GrantexFastAPIError, grantex_exception_handler)

grantex = GrantexAuth(
    jwks_uri="https://grantex-auth-dd4mtrt2gq-uc.a.run.app/.well-known/jwks.json",
    clock_tolerance=5,
)


# Public health check
@app.get("/health")
async def health():
    return {"status": "ok"}


# Token verification only
@app.get("/api/me")
async def me(grant: VerifiedGrant = Depends(grantex)):
    return {
        "principalId": grant.principal_id,
        "agentDid": grant.agent_did,
        "scopes": list(grant.scopes),
    }


# Scope enforcement
@app.get("/api/calendar")
async def calendar(grant: VerifiedGrant = Depends(grantex.scopes("calendar:read"))):
    return {"events": get_calendar_events(grant.principal_id)}


@app.post("/api/calendar/events")
async def create_event(grant: VerifiedGrant = Depends(grantex.scopes("calendar:write"))):
    return {"created": True}


@app.post("/api/email/send")
async def send_email(
    grant: VerifiedGrant = Depends(grantex.scopes("email:read", "email:send")),
):
    return {"sent": True}


def get_calendar_events(principal_id: str) -> list[dict]:
    return [{"id": "1", "title": "Team standup", "principalId": principal_id}]

API Reference

GrantexAuth(jwks_uri, *, clock_tolerance=0, audience=None, token_extractor=None)

Creates a Grantex authentication dependency.
ParameterTypeDefaultDescription
jwks_uristrrequiredJWKS endpoint URL for token verification
clock_toleranceint0Seconds of clock skew tolerance
audiencestr | NoneNoneExpected JWT aud claim
token_extractorCallable[[Request], str | None] | NoneNoneCustom function to extract the token

grantex.scopes(*required_scopes)

Returns a dependency that verifies the token AND checks all required scopes.

require_scopes(grant, *scopes)

Standalone function that checks scopes on an already-verified grant. Raises GrantexFastAPIError with SCOPE_INSUFFICIENT if any scope is missing.

grantex_exception_handler(request, exc)

Starlette exception handler that converts GrantexFastAPIError to a JSON response.

Requirements

  • Python 3.9+
  • FastAPI >= 0.100.0
  • grantex >= 0.1.0