> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grantex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# FastAPI

> Grant token verification and scope-based authorization for FastAPI using dependency injection.

## Install

```bash theme={null}
pip install grantex-fastapi
```

## Quick Start

One dependency protects your entire API:

```python theme={null}
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:

```python theme={null}
@app.get("/api/me")
async def me(grant: VerifiedGrant = Depends(grantex)):
    return {"principalId": grant.principal_id, "agentDid": grant.agent_did}
```

***

## Scope Enforcement

### Via dependency (recommended)

Use `Depends(grantex.scopes(...))` to verify the token AND check scopes in one step:

```python theme={null}
# 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()`:

```python theme={null}
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`:

<CodeGroup>
  ```python Cookie theme={null}
  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)
  ```

  ```python Custom Header theme={null}
  def extract_from_header(request: Request) -> str | None:
      return request.headers.get("x-grant-token")

  grantex = GrantexAuth(jwks_uri=JWKS_URI, token_extractor=extract_from_header)
  ```

  ```python Query Parameter theme={null}
  def extract_from_query(request: Request) -> str | None:
      return request.query_params.get("token")

  grantex = GrantexAuth(jwks_uri=JWKS_URI, token_extractor=extract_from_query)
  ```
</CodeGroup>

***

## Error Handling

Register the built-in exception handler to return JSON error responses:

```python theme={null}
app.add_exception_handler(GrantexFastAPIError, grantex_exception_handler)
```

Or write a custom handler:

```python theme={null}
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

| Code                 | HTTP Status | When                                         |
| -------------------- | ----------- | -------------------------------------------- |
| `TOKEN_MISSING`      | 401         | No token found in the request                |
| `TOKEN_INVALID`      | 401         | JWT signature or format is invalid           |
| `TOKEN_EXPIRED`      | 401         | JWT `exp` claim is in the past               |
| `SCOPE_INSUFFICIENT` | 403         | Token is missing one or more required scopes |

***

## `grant` Reference

The `VerifiedGrant` dataclass contains:

| Field              | Type              | Description                               |
| ------------------ | ----------------- | ----------------------------------------- |
| `token_id`         | `str`             | JWT `jti` claim — unique token identifier |
| `grant_id`         | `str`             | Grantex grant record ID                   |
| `principal_id`     | `str`             | End-user who authorized the agent         |
| `agent_did`        | `str`             | Agent's decentralized identifier          |
| `developer_id`     | `str`             | Developer organization ID                 |
| `scopes`           | `tuple[str, ...]` | Scopes the agent was granted              |
| `issued_at`        | `int`             | Token issued-at (seconds since epoch)     |
| `expires_at`       | `int`             | Token expiry (seconds since epoch)        |
| `parent_agent_did` | `str \| None`     | Parent agent DID (delegated grants only)  |
| `parent_grant_id`  | `str \| None`     | Parent grant ID (delegated grants only)   |
| `delegation_depth` | `int \| None`     | Delegation depth (0 = root grant)         |

***

## Full Example

A complete FastAPI application protected by Grantex:

```python theme={null}
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.

| Parameter         | Type                                       | Default    | Description                              |
| ----------------- | ------------------------------------------ | ---------- | ---------------------------------------- |
| `jwks_uri`        | `str`                                      | *required* | JWKS endpoint URL for token verification |
| `clock_tolerance` | `int`                                      | `0`        | Seconds of clock skew tolerance          |
| `audience`        | `str \| None`                              | `None`     | Expected JWT `aud` claim                 |
| `token_extractor` | `Callable[[Request], str \| None] \| None` | `None`     | Custom 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
