Metadata-Version: 2.5
Name: vercel-passport-bundle
Version: 0.1.0
Summary: Passport identity helpers for Vercel Python applications
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: pydantic<3,>=2.10.0
Requires-Dist: vercel-headers-bundle>=0.7.2
Requires-Dist: vercel-oidc-bundle[verify]>=0.11.0
Description-Content-Type: text/markdown

# vercel-passport-bundle

This is a version of `vercel-passport` with third-party dependencies bundled. For normal use, install the unbundled `vercel-passport` package instead: https://pypi.org/project/vercel-passport/

# Passport

`vercel.passport` reads and verifies [Vercel Passport](https://vercel.com/docs)
identities in Python functions.

```bash
uv add vercel-passport
```

## Read the current visitor

```python
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from vercel.passport import get_identity

app = FastAPI()


@app.get("/api/me")
async def me():
    identity = await get_identity()
    if identity is None:
        return JSONResponse({"authenticated": False}, status_code=401)

    return {
        "authenticated": True,
        "external_subject": identity.external_subject,
        "subject": identity.subject,
    }
```

`get_identity()` reads the `x-vercel-oidc-passport-token` header that Vercel adds
after Passport authenticates the visitor, falling back to the `_vercel_passport`
cookie. A session too large for one cookie is split across several, and is
reassembled and checksummed before it is verified. With no arguments, it reads
the headers the Vercel Python runtime records for the current request. You can
also pass the request, or its headers, explicitly:

```python
identity = await get_identity(request)
identity = await get_identity(request.headers, cookies=request.cookies)
```

A token from the request is always verified against Vercel's OIDC JWKS. Only the
Passport issuer, `https://passport.vercel.com/<owner>`, is accepted, and the
token must have the Passport shape, so a regular Vercel OIDC token is never
accepted as a Passport identity.

By default the token must match this deployment's project and environment, read
from `VERCEL_PROJECT_ID` and `VERCEL_TARGET_ENV` (or `VERCEL_ENV`). If neither can
be determined, verification fails closed. Pass `project_id`, `environment`,
`owner_id`, or `audience` to set the expectations explicitly. Time claims allow
60 seconds of clock skew.

### Differences from `@vercel/passport`

Expectations compare by equality, so a backend cannot accidentally accept tokens
minted for any project:

- `"*"` is not a wildcard for `project_id` or `environment`; it matches only a
  token whose claim is literally `"*"`.
- Lists of accepted projects or environments are not supported. Call
  `verify_identity()` once per expected project instead.
- Of `jose`'s verify options, only `audience` is exposed, and the clock-skew
  allowance is fixed at 60 seconds. That allowance also applies to `iat`: a token
  claiming to be issued more than 60 seconds in the future is rejected, where
  `jose` does not check `iat` at all.
- An unverified debugging token is accepted only in local development by default.
  See [Local development](#local-development).
- The synthesized development identity is opt-in with `VERCEL_PASSPORT_DEV=1`
  rather than on by default, so a process that is merely not on Vercel, such as
  a self-hosted server, never authenticates every visitor as a test user.
- Local identities are set with the `local_identity()` context manager or
  `VERCEL_PASSPORT_IDENTITY` rather than `getIdentity` options, and are ignored
  on Vercel deployments other than `VERCEL_ENV=development`.
- `identity.project` is set only when the token names a project ID, which every
  verified token does, so `identity.project.id` is never `None`.
- In `PassportDevelopmentIdentity`, an explicit `None` clears a field its
  `VERCEL_PASSPORT_DEV_*` variable sets.

Identities are frozen Pydantic models, so an endpoint can return one directly, or
serialize it with `identity.model_dump()`. The bearer token is left out of both
`model_dump()` and the repr, so neither a response nor a log line exposes it;
read it as `identity.token` when you need to forward it.

Use `require_identity()` when a route needs a real Passport token. It takes the
same arguments as `get_identity()`, but raises `PassportIdentityError` instead of
returning `None`, and also rejects an identity without a token, such as the
local development identity. The returned identity's `token` is always set:

```python
from vercel.passport import PassportIdentityError, require_identity

try:
    identity = await require_identity()
except PassportIdentityError:
    raise HTTPException(status_code=401)

forward_to_backend(identity.token)
```

For synchronous code, such as Flask or Django views, use `vercel.passport.sync`,
which has the same names and arguments:

```python
from vercel.passport.sync import get_identity

identity = get_identity()
```

## Verify forwarded tokens

Use `verify_identity()` when a Passport-protected app forwards its Passport token
to another backend. It accepts the token itself, or request headers carrying an
`Authorization: Bearer <token>` header:

```python
from fastapi import FastAPI, HTTPException, Request
from vercel.passport import PassportIdentityError, verify_identity

app = FastAPI()


@app.post("/api/orders")
async def orders(request: Request):
    try:
        identity = await verify_identity(
            request,
            owner_id="team_123",
            project_id="prj_123",
            environment="production",
        )
    except PassportIdentityError:
        raise HTTPException(status_code=401)

    return {"external_subject": identity.external_subject, "email": identity.email}
```

Unlike `get_identity()`, `verify_identity()` always verifies and never falls back
to a local identity. If the token was minted by a different Vercel project than
the backend verifying it, pass the source project's `project_id` and
`environment`.

## Local development

Local development usually has no Passport header or cookie, because Passport
creates them in Vercel's edge network, so `get_identity()` returns `None` there.
To work locally as a signed-in visitor, opt in to a synthesized test identity:

```bash
VERCEL_PASSPORT_DEV=1
```

`get_identity()` then returns a test identity when the request has no token, and
emits a `PassportDevelopmentIdentityWarning` the first time it does so. The test
identity uses:

```txt
external_subject: test-user
email: test-user@passport.local
name: Test User
```

Override individual fields with environment variables:

```bash
VERCEL_PASSPORT_DEV_OWNER=acme
VERCEL_PASSPORT_DEV_CONNECTOR_ID=scl_dev
VERCEL_PASSPORT_DEV_EXTERNAL_SUB=user_dev
VERCEL_PASSPORT_DEV_EXTERNAL_ISS=https://idp.example.com
VERCEL_PASSPORT_DEV_PROJECT=my-project
VERCEL_PASSPORT_DEV_PROJECT_ID=prj_local
```

To set the identity in code, such as in tests, use `local_identity()`. Inside the
block, requests without a token get that identity, with no environment variable
needed. Pass a `PassportDevelopmentIdentity`, whose unset fields still come from
the environment, or the full claims yourself:

```python
from vercel.passport import PassportDevelopmentIdentity, local_identity

with local_identity(PassportDevelopmentIdentity(owner="acme", external_subject="user_dev")):
    response = client.get("/api/me")

with local_identity({
    "typ": "passport",
    "iss": "https://passport.vercel.com/local",
    "owner": "local",
    "connector_id": "local",
    "external_sub": "local-user",
    "sub": "owner:local:connector:local:principal:local-user",
}):
    ...
```

The identity is held in a context variable, so it applies to the current
request or test, to tasks started inside the block, and to threads started with
`anyio.to_thread` or `asyncio.to_thread`, but not a plain `threading.Thread`.
Full claims
can also be given as JSON in `VERCEL_PASSPORT_IDENTITY`. The identity set with
`local_identity()` comes first, then `VERCEL_PASSPORT_IDENTITY`, then the
`VERCEL_PASSPORT_DEV=1` test identity.

None of these is used on a Vercel deployment unless `VERCEL_ENV=development`,
and none is verified, so `identity.verified` is `False`.

To debug with a real token copied from a deployed request, pass it directly or
set `VERCEL_PASSPORT_TOKEN`. Such a token is decoded without verification
unless you pass `verify=True`:

```python
identity = await get_identity(token=copied_token)
```

Unverified tokens are accepted only in local development. On a Vercel
deployment, other than `VERCEL_ENV=development`, an unverified token raises
`PassportIdentityError`, including one left in `VERCEL_PASSPORT_TOKEN`. In
deployed code, use `verify_identity()` or pass `verify=True` to verify explicit
tokens. Pass `allow_unverified=True` only if you really mean to trust an unsigned
token.
