Skip to main content

auth-paseto

PyPI Python License

Access/refresh token authentication built on PASETO (Platform-Agnostic Security Tokens), a safer alternative to JWT. Framework-agnostic, with first-class support for async frameworks such as FastAPI.

Features

  • PASETO v1–v4 (defaults to v4), powered by pyseto.
  • Both purposes: local (symmetric encryption) and public (asymmetric signatures).
  • Access and refresh tokens with independent lifetimes and type checking on verification.
  • Fresh tokens for sensitive operations (e.g. password changes).
  • Revocation through an async callback (e.g. a denylist in Redis/Valkey).
  • Typed: Pydantic configuration and a py.typed marker.

Installation

pip install auth-paseto
# or
uv add auth-paseto

Requires Python 3.12+.

Quick start

import asyncio
import secrets

from auth_paseto import AuthConfig, PASETOAuth

auth = PASETOAuth(AuthConfig(secret_key=secrets.token_urlsafe(32)))

token = auth.create_access_token(subject="user_123", user_claims={"role": "admin"})
payload = asyncio.run(auth.get_access_token_payload(token))

print(payload["sub"], payload["role"])  # user_123 admin

Configuration

Local (symmetric) tokens

from auth_paseto import AuthConfig, TokenPurpose

config = AuthConfig(
    secret_key="<at least 32 random bytes>",  # e.g. secrets.token_urlsafe(32)
    purpose=TokenPurpose.LOCAL,  # default
)

Use a long, randomly generated secret loaded from your environment or secret manager; never commit it.

Public (asymmetric) tokens

Tokens are signed with the private key and verified with the public key. Keys are PEM-encoded and can be given inline (private_key, public_key) or as file paths:

from auth_paseto import AuthConfig, TokenPurpose

config = AuthConfig(
    purpose=TokenPurpose.PUBLIC,
    private_key_file="keys/private.pem",
    public_key_file="keys/public.pem",
)

For v4 (the default), generate an Ed25519 key pair:

openssl genpkey -algorithm ed25519 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem

Options

Option Type Default Description
secret_key str None Symmetric key. Required for local.
private_key / private_key_file str None PEM private key (or path to it). Required for public.
public_key / public_key_file str None PEM public key (or path to it). Required for public.
purpose TokenPurpose LOCAL local or public.
version PASETOVersion v4 PASETO protocol version (1–4).
access_token_expires int | timedelta | False 30 min Access token lifetime (seconds if int). False disables expiry.
refresh_token_expires int | timedelta 15 days Refresh token lifetime (seconds if int).
decode_leeway int | timedelta 2 Clock-skew tolerance when verifying, in seconds.
encode_issuer str None Sets the iss claim on issued tokens.
encode_audience str "" Sets the aud claim on issued tokens (omitted if empty).
decode_issuer str None If set, verification rejects tokens whose iss claim differs.
decode_audience str None If set, verification rejects tokens whose aud claim differs.

Usage

Creating tokens

access = auth.create_access_token(subject="user_123", user_claims={"role": "admin"})
fresh = auth.create_access_token(subject="user_123", fresh=True)
refresh = auth.create_refresh_token(subject="user_123")

Every token carries sub, jti (a UUID4), type, iat, nbf and exp claims; access tokens also carry fresh. Pass base64_encode=True to get the token base64-encoded.

Verifying tokens

The verification methods are coroutines that return the decoded payload as a dict:

payload = await auth.get_access_token_payload(token)        # access tokens only
payload = await auth.get_fresh_access_token_payload(token)  # fresh access tokens only
payload = await auth.get_refresh_token_payload(token)       # refresh tokens only

On failure they raise TokenVerificationError. Its detail attribute is a Pydantic model with a machine-readable type (ErrorCode) and a human-readable reason:

ErrorCode Raised when
TOKEN_MISSING The token is empty or None.
TOKEN_INVALID_ERROR The token is malformed, tampered with, expired, not yet valid, or has the wrong issuer/audience.
UNEXPECTED_TOKEN_TYPE E.g. a refresh token was passed where an access token was expected.
EXPECTED_FRESH_TOKEN A fresh token was required but the token is not fresh.
TOKEN_REVOKED The revocation callback reported the token as revoked.
from auth_paseto import ErrorCode, TokenVerificationError

try:
    payload = await auth.get_access_token_payload(token)
except TokenVerificationError as e:
    if e.detail.type == ErrorCode.TOKEN_INVALID_ERROR:
        ...

Revocation

Pass an async callback that receives the token's jti and returns True if the token is revoked. It runs on every verification.

async def is_revoked(jti: str) -> bool:
    return await redis.exists(f"revoked:{jti}") > 0

auth = PASETOAuth(config, is_revoked_callback=is_revoked)

To revoke a token (e.g. on logout), store its jti until the token would have expired:

payload = await auth.get_refresh_token_payload(token)
await redis.set(f"revoked:{payload['jti']}", 1, ex=auth.refresh_token_expires)

FastAPI integration

auth-paseto has no dependency on FastAPI. Wire it in with a token extractor, a dependency and an exception handler:

from typing import Annotated, Any

from fastapi import Depends, FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordBearer

from auth_paseto import AuthConfig, PASETOAuth, TokenVerificationError

auth = PASETOAuth(AuthConfig(secret_key=os.environ["AUTH_SECRET_KEY"]))
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")

app = FastAPI()


@app.exception_handler(TokenVerificationError)
async def token_error_handler(request: Request, exc: TokenVerificationError):
    return JSONResponse(
        status_code=401,
        content=exc.detail.model_dump(mode="json"),
        headers={"WWW-Authenticate": "Bearer"},
    )


async def access_payload(token: Annotated[str, Depends(oauth2_scheme)]) -> dict[str, Any]:
    return await auth.get_access_token_payload(token)


async def fresh_payload(token: Annotated[str, Depends(oauth2_scheme)]) -> dict[str, Any]:
    return await auth.get_fresh_access_token_payload(token)


@app.post("/login")
async def login():
    # ... authenticate the user first ...
    user_id = "user_123"
    return {
        "access_token": auth.create_access_token(subject=user_id, fresh=True),
        "refresh_token": auth.create_refresh_token(subject=user_id),
        "token_type": "bearer",
    }


@app.get("/me")
async def me(payload: Annotated[dict[str, Any], Depends(access_payload)]):
    return {"user_id": payload["sub"]}


@app.post("/change-password")
async def change_password(payload: Annotated[dict[str, Any], Depends(fresh_payload)]):
    return {"message": "Access granted to sensitive operation"}

Development

git clone https://gitlab.com/Soralsei/auth_paseto.git
cd auth_paseto
uv sync
uv run pytest

License

MIT

Metadata

Release files for auth-paseto 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 auth-paseto 0.1.0
File Size Uploaded
auth_paseto-0.1.0.tar.gz 7.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for auth-paseto 0.1.0
File Interpreter ABI Platform
auth_paseto-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 16.7 kB

Release files / auth_paseto-0.1.0.tar.gz

Download URL auth_paseto-0.1.0.tar.gz
Size 7.4 kB
Tags Source
SHA-256 checksum
How to use checksums
f6ea38c13c6341d693699b0087a2748e121e3c5c260f0899fe9ee63c6be7f804
BLAKE2b-256 checksum
How to use checksums
bc2429edce40d3435a314d5c2b5c85e39085c7b6584e49a6ba1908643072b402
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

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

Download URL auth_paseto-0.1.0-py3-none-any.whl
Size 9.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
35483913a55d7e665201f5cd266f3a86defc60af54901ebc5b6fff02ab2e8f4e
BLAKE2b-256 checksum
How to use checksums
a33d922dcc311cefe24a1e9f0c9c5b5f62678da8811920205042ada29a9dfb74
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.2.1

2 release files

0.2.0

2 release files

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