Metadata-Version: 2.4
Name: authaction-python-sdk
Version: 0.1.0
Summary: AuthAction JWT verification SDK for Python — Django, Flask, and FastAPI
License: MIT
Keywords: oauth2,jwt,jwks,authentication,authaction,django,flask,fastapi
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: PyJWT[crypto]>=2.8.0
Provides-Extra: django
Requires-Dist: django>=3.2; extra == "django"
Requires-Dist: djangorestframework>=3.14; extra == "django"
Provides-Extra: flask
Requires-Dist: flask>=2.3; extra == "flask"
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100; extra == "fastapi"
Requires-Dist: httpx>=0.24; extra == "fastapi"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-mock>=3.12; extra == "dev"
Requires-Dist: django>=3.2; extra == "dev"
Requires-Dist: djangorestframework>=3.14; extra == "dev"
Requires-Dist: flask>=2.3; extra == "dev"
Requires-Dist: fastapi>=0.100; extra == "dev"
Requires-Dist: httpx>=0.24; extra == "dev"

# authaction-python-sdk

JWT verification SDK for Python backends. Validates AuthAction access tokens via JWKS — handles key fetching, caching, and rotation automatically.

Works with **Django REST Framework**, **Flask**, and **FastAPI**.

## Installation

```bash
# Core only
pip install authaction-python-sdk

# With Django support
pip install "authaction-python-sdk[django]"

# With Flask support
pip install "authaction-python-sdk[flask]"

# With FastAPI support
pip install "authaction-python-sdk[fastapi]"
```

---

## Core

```python
from authaction import AuthAction

aa = AuthAction(
    domain=os.getenv("AUTHACTION_DOMAIN"),    # e.g. myapp.eu.authaction.com
    audience=os.getenv("AUTHACTION_AUDIENCE"), # e.g. https://api.myapp.com
)

# Verify a raw token — raises TokenExpiredError / TokenInvalidError on failure
payload = aa.verify_token(token)

# Verify from Authorization header — returns None on missing/invalid, never raises
payload = aa.verify_request(request.headers.get("Authorization"))

print(payload["sub"])   # user identifier
print(payload["email"]) # any JWT claim
```

---

## Django REST Framework

### 1. Configure settings

```python
# settings.py
AUTHACTION = {
    "DOMAIN":   os.getenv("AUTHACTION_DOMAIN"),
    "AUDIENCE": os.getenv("AUTHACTION_AUDIENCE"),
}

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "authaction.django.AuthActionAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}
```

### 2. Use in views

```python
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response

@api_view(["GET"])
@permission_classes([AllowAny])
def public_view(request):
    return Response({"message": "Public"})

@api_view(["GET"])
def protected_view(request):
    return Response({"sub": request.user.sub, "email": request.user.email})
```

`request.user` is an `AuthenticatedToken` — access any JWT claim as an attribute:

```python
request.user.sub        # str
request.user.email      # any claim
request.user.payload    # dict — all raw claims
```

---

## Flask

```python
from authaction import AuthAction
from authaction.flask import make_require_auth

aa = AuthAction(
    domain=os.getenv("AUTHACTION_DOMAIN"),
    audience=os.getenv("AUTHACTION_AUDIENCE"),
)
require_auth = make_require_auth(aa)

@app.get("/public")
def public_route():
    return {"message": "Public"}

@app.get("/protected")
@require_auth
def protected_route():
    from flask import g
    return {"sub": g.current_user["sub"]}
```

The decoded payload is available as `g.current_user` (a `dict`) inside decorated routes.

---

## FastAPI

```python
from fastapi import FastAPI, Depends
from authaction import AuthAction
from authaction.fastapi import make_require_auth

aa = AuthAction(
    domain=os.getenv("AUTHACTION_DOMAIN"),
    audience=os.getenv("AUTHACTION_AUDIENCE"),
)
require_auth = make_require_auth(aa)

app = FastAPI()

@app.get("/public")
def public_route():
    return {"message": "Public"}

@app.get("/protected")
def protected_route(user: dict = Depends(require_auth)):
    return {"sub": user["sub"], "email": user.get("email")}
```

### Scope enforcement

```python
require_admin = make_require_auth(aa, scopes=["admin"])

@app.delete("/users/{user_id}")
def delete_user(user_id: str, user: dict = Depends(require_admin)):
    ...  # raises HTTP 403 if token lacks 'admin' scope
```

---

## Exceptions

```python
from authaction.exceptions import TokenExpiredError, TokenInvalidError

try:
    payload = aa.verify_token(token)
except TokenExpiredError:
    # token exp claim is in the past
    ...
except TokenInvalidError:
    # bad signature, wrong issuer/audience, malformed JWT
    ...
```

## Environment variables

```bash
AUTHACTION_DOMAIN=your-tenant.eu.authaction.com
AUTHACTION_AUDIENCE=https://api.your-app.com
```

## How JWKS caching works

Uses `PyJWT`'s `PyJWKClient` which:
- Fetches public keys from `https://<domain>/.well-known/jwks.json` on first use
- Caches up to 16 keys in-process (LRU, configurable via `jwks_cache_keys`)
- Automatically re-fetches when an unknown `kid` is encountered (key rotation)

## License

MIT
