Skip to main content

authgate-client

CI PyPI Python License: MIT

Verify AuthGate access tokens in Python. Framework-free, with one dependency — PyJWT and its crypto extra — and an optional FastAPI integration. Installed as authgate-client, imported as authgate.

from typing import Annotated

from fastapi import Depends, FastAPI

from authgate import AuthGate, Identity

auth = AuthGate(issuer="https://auth.example.com", audience="https://api.example.com")
app = FastAPI()


@app.get("/materials")
def list_materials(caller: Annotated[Identity, Depends(auth.identity)]):
    return materials.for_tenant(caller.tenant_id)


@app.delete("/materials/{material_id}")
def delete_material(material_id: str, caller: Annotated[Identity, Depends(auth.require("material", "delete"))]):
    materials.delete(material_id, by=caller.account_id)
pip install authgate-client              # the verifier
pip install "authgate-client[fastapi]"   # plus the FastAPI dependencies

You may not need this

AuthGate publishes a standard /.well-known/openid-configuration and JWKS, so any JWT library that can fetch a JWKS already verifies its tokens. This package is a convenience, the Python counterpart of the server's Spring starter. What it adds is what a hand-written verifier usually gets wrong:

  • The audience is required. A verifier that skips aud accepts every token the issuer ever minted, including tokens meant for a different service. There is no default for "any".
  • A typed caller. The perms claim arrives as Identity.permissions, and caller.may("material", "update") applies the server's own rule, superuser included.
  • Key refresh that cannot be turned against the issuer. Tokens naming an unknown kid trigger at most one JWKS fetch every 30 seconds, however many arrive.
  • An outage is not a bad token. If the issuer cannot be reached, the answer is 503, not 401 — a 401 tells well-behaved clients to throw their token away and sign the user out.

Without FastAPI

verify is plain synchronous code, so it works in Flask, Django, a worker or a script:

from authgate import AuthGate, InvalidTokenError, IssuerUnavailableError

auth = AuthGate(issuer="https://auth.example.com", audience="https://api.example.com")

try:
    caller = auth.verify(token)
except InvalidTokenError:
    ...  # 401
except IssuerUnavailableError:
    ...  # 503

if not caller.may("material", "update"):
    ...  # 403

It is also why the FastAPI dependencies are plain defs: FastAPI runs them in its threadpool, so the occasional key fetch never blocks the event loop. With the keys cached, verifying a token is normally a signature check and nothing else.

Testing an app that uses it

Override auth.identity to stand in a caller. Every require() builds on it, so the permission checks still run against the identity you supply:

app.dependency_overrides[auth.identity] = lambda: Identity("acct-1", permissions={"material": frozenset({"read"})})

The dependencies are for HTTP routes, since they read the Authorization header. Use them with Depends, not Security(..., scopes=...): scopes make FastAPI treat each use as a separate dependency, and the token would be verified once per use instead of once per request.

What is checked

Check Rule
Signature RS256 only. The algorithm is pinned, never read from the token header, which closes alg: none and the RSA-key-as-HMAC-secret confusion. It is checked before any key lookup, so such a token is refused as invalid even while the issuer is down
Key the kid header must name an RS256 signing key the issuer publishes. If the issuer cannot be reached to look one up, the answer is 503, not 401
Issuer iss must equal the discovery document's issuer exactly, and that document must name the issuer you configured
Audience aud must contain the audience you configured
Lifetime exp and iat required, nbf honoured if present, with 60 seconds of clock skew allowed (leeway=, up to 300). An iat further in the future than that is rejected — stricter than Spring, which checks only exp and nbf
Required claims exp, iat, iss, aud, sub, jti
Transport the issuer must be https://host[:port][/path]; plain http:// is accepted only for localhost, 127.0.0.1 and ::1

Keys are fetched on first use — constructing an AuthGate does no I/O — and cached for five minutes. A failed refresh keeps serving the cached keys rather than failing every request — until a refresh succeeds, however long that takes, so a key the issuer withdraws during an outage is trusted until the verifier can see that it is gone.

The caller

Identity field Claim
account_id sub AuthGate's account id, not the external provider's subject
tenant_id tenant
email email for display and audit
superuser superuser True only for a literal JSON true
permissions perms {resource: frozenset(actions)}
claims all of them the verified payload; not part of equality, so two tokens for one caller compare equal

Errors

Raised Meaning FastAPI response
— no Authorization: Bearer header at all 401, WWW-Authenticate: Bearer
InvalidTokenError the token is not acceptable here 401, WWW-Authenticate: Bearer error="invalid_token"
IssuerUnavailableError the issuer's keys cannot be fetched 503, with a fixed message; the detail is logged
ConfigurationError the verifier is set up wrong, or the issuer's discovery document contradicts it raised at construction; 500 if discovered later
— a missing permission in require() 403, WWW-Authenticate: Bearer error="insufficient_scope"

All three exceptions derive from AuthGateError.

The public API is what authgate exports — AuthGate, Identity and the errors above. The submodules are implementation, and may change between releases.

Revocation

AuthGate revokes an access token by adding its jti to a denylist on the server. A verifier working from the published keys cannot see that list, so a token that was already issued stays valid here until it expires. The server's short access-token lifetime — 15 minutes by default — is what bounds that window, and revocation is enforced where a new token would be issued. Every JWT-issuing provider behaves this way.

Development

pip install -e ".[dev]" "ruff==0.16.9"   # the ruff CI pins
pytest
ruff check . && ruff format --check .

The tests run against an in-process fake issuer: a real RSA key, a real JWKS served over real HTTP on 127.0.0.1, and tokens carrying exactly the claims the server issues. That is what makes each attack — alg: none, HMAC confusion, a foreign key reusing a known kid, a tampered payload — a short test with no external network.

License

MIT — see LICENSE.

Metadata

Release files for authgate-client 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for authgate-client 0.1.0
File Size Uploaded
authgate_client-0.1.0.tar.gz 21.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for authgate-client 0.1.0
File Interpreter ABI Platform
authgate_client-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 36.7 kB

Release files / authgate_client-0.1.0.tar.gz

Download URL authgate_client-0.1.0.tar.gz
Size 21.9 kB
Tags Source
SHA-256 checksum
How to use checksums
ddda563fc575b9b9fdee77abade2f071316bf6a42b7cd4f232cfd3ecad7f7294
BLAKE2b-256 checksum
How to use checksums
af91361a8c2da8ffe13d98d828414ea3c754ee4d9aab5734b3857d5a9a3bd408
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / authgate_client-0.1.0-py3-none-any.whl

Download URL authgate_client-0.1.0-py3-none-any.whl
Size 14.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f106c800d2855981cda6896eec7d283feab2edf92dc32d4b1fdee89657c3d2ff
BLAKE2b-256 checksum
How to use checksums
d3ff20bb5384443065f19f47965e2b80cfeef53ec96ef77ae5f34f96077fea4f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page