Metadata-Version: 2.4
Name: oxaigen-auth
Version: 0.0.5
Summary: Oxaigen Auth SDK
Author: Luca Roggeveen
Author-email: luca@oxaigen.com
Requires-Python: >=3.10,<3.13
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Dist: fastapi (>=0.118)
Requires-Dist: httpx (>=0.28.1)
Requires-Dist: pydantic (>=2.0)
Description-Content-Type: text/markdown

# Oxaigen Auth SDK Guide

This project is a minimal FastAPI backend that demonstrates how to protect endpoints with the `oxaigen-auth` SDK.

Use this document as a shareable integration guide for teams that want to add Oxaigen authentication to their own backend app.

## What this SDK gives you

- Request middleware that enriches each request with auth context.
- A `get_current_user` dependency for authenticated routes.
- A `get_current_user_optional` dependency for mixed public/private routes.
- A `require_permission("<permission>")` dependency for role/permission checks.
- A typed `User` model injected into your endpoint handlers.

## Requirements

- Python `3.11+`
- FastAPI app
- An Oxaigen environment/proxy available for token verification

## Install

With Poetry:

```bash
poetry add oxaigen-auth
```

With pip:

```bash
pip install oxaigen-auth
```

## Import paths (recommended)

```python
from oxaigen import (
    User,
    get_current_user,
    get_current_user_optional,
    require_permission,
    OxaigenAuthMiddleware,
)
```
Available root exports include:

- Main API: `User`, `get_current_user`, `get_current_user_optional`, `require_permission`, `OxaigenAuthMiddleware`
- Settings and exceptions: `OxaigenAuthSettings`, `OxaigenAuthUnauthenticatedError`, `OxaigenAuthPermissionDeniedError`, `OxaigenAuthUpstreamError`
- Advanced utilities: `ProxyAuthClient`, `aclose_client`, `extract_bearer_token`, `derive_proxy_base_url`

## Minimal FastAPI integration

```python
from typing import Optional
from fastapi import FastAPI, Depends
from oxaigen_auth import OxaigenAuthMiddleware
from oxaigen_auth import (
    User,
    get_current_user,
    get_current_user_optional,
    require_permission,
)

app = FastAPI()
app.add_middleware(OxaigenAuthMiddleware)

@app.get("/v1/me")
async def me(user: User = Depends(get_current_user)):
    return user

@app.get("/v1/maybe-public")
async def maybe_public(user: Optional[User] = Depends(get_current_user_optional)):
    if user:
        return {"signed_in": True, "email": user.email}
    return {"signed_in": False}

@app.get("/v1/audit")
async def audit(user: User = Depends(require_permission("audit"))):
    return {"audit_log": [{"hello":"world"}]}
```

## Behavior model

Important: middleware and dependencies have different responsibilities.

- `OxaigenAuthMiddleware`:
  - Parses incoming auth context.
  - Enriches request state with user/auth metadata when present.
  - Does **not** block unauthenticated requests on its own.

- `get_current_user`:
  - Enforces authentication.
  - Returns the authenticated `User`.
  - Fails the request when token/user validation fails.

- `get_current_user_optional`:
  - Returns `User` when token is valid.
  - Returns `None` when token is missing/invalid.
  - Still fails with `502` when auth upstream is unavailable.

- `require_permission("...")`:
  - Enforces both authentication and permission presence.
  - Use for routes that require explicit capabilities (for example `audit`).
  - Validates permission name format: `[A-Za-z0-9_-]+`.

## Authenticated request flow

1. Client sends request with auth cookie/token.
2. Middleware runs and prepares auth request context.
3. Endpoint dependency (`get_current_user` or `require_permission`) validates against Oxaigen auth backend.
4. Endpoint executes with resolved `User`.

Token extraction order:

1. `Authorization: Bearer <token>` header
2. Auth cookie (configured name)

## Caching behavior

The SDK caches per process to reduce upstream traffic:

- User cache: token hash -> `User` (`CACHE_TTL_SECONDS`, default `30s`)
- Permission cache: token hash + permission -> bool (`CACHE_TTL_SECONDS`, default `30s`)
- Negative cache for invalid tokens: token hash sentinel (`NEGATIVE_CACHE_TTL_SECONDS`, default `5s`)

Notes:

- Cache keys use a hash of the token (not the raw token).
- Invalid token bursts are throttled by the negative cache.
- Middleware and dependencies share the same validation/cache pipeline.

## SDK configuration (optional)

All env vars are optional with safe defaults:

| Var                                       | Default                          | Purpose                                                          |
|-------------------------------------------|----------------------------------|------------------------------------------------------------------|
| `OXAIGEN_AUTH_PROXY_URL_OVERRIDE`         | unset                            | Force a specific proxy URL. Default: derived from `Host` header. |
| `OXAIGEN_AUTH_PROXY_SCHEME`               | `https`                          | Scheme when deriving proxy URL. Set `http` for dev.              |
| `OXAIGEN_AUTH_ACCESS_TOKEN_COOKIE_NAME`   | `OxaigenPlatformAuthAccessToken` | Match if the proxy uses a custom cookie name.                    |
| `OXAIGEN_AUTH_CACHE_TTL_SECONDS`          | `30`                             | Validation cache TTL.                                            |
| `OXAIGEN_AUTH_NEGATIVE_CACHE_TTL_SECONDS` | `5`                              | TTL for failed-validation entries.                               |
| `OXAIGEN_AUTH_CACHE_MAX_ENTRIES`          | `1024`                           | LRU cap.                                                         |
| `OXAIGEN_AUTH_PROXY_TIMEOUT_SECONDS`      | `5.0`                            | Server-to-server HTTP timeout.                                   |


Tip: for local/dev environments behind non-standard host routing, `OXAIGEN_AUTH_PROXY_URL_OVERRIDE` is commonly the most useful setting.

## Local auth mock server

The package includes a development mock server at `oxaigen_auth/dev/mock_server.py` that emulates the proxy's `/_oxa_auth/*` API surface, including popup login flow and token cookies.

Start it with:

```bash
poetry run oxaigen-auth-mock
```

By default it binds to `127.0.0.1:8765`.

### What it mocks

- `GET /_oxa_auth/login`
- `GET /_oxa_auth/mock-authorize` (mock IdP authorize redirect)
- `GET /_oxa_auth/callback` (callback page that posts `OXA_AUTH_SUCCESS`/`OXA_AUTH_FAILURE`)
- `POST /_oxa_auth/token-exchange`
- `POST /_oxa_auth/refresh`
- `POST /_oxa_auth/test-app-token`
- `GET /_oxa_auth/get-me`
- `POST /_oxa_auth/test-app-permission/{name}`
- `POST /_oxa_auth/logout`

### Frontend SDK usage with the mock

For `@oxaigen/react`, point auth calls at the mock host:

```tsx
<AuthProvider authApiBaseUrl="http://127.0.0.1:8765" />
```

This is a cross-origin dev setup. The popup, authorize redirect, callback, and token-exchange all run on the mock host; `postMessage` returns control to the app window.

### Common mock env vars

The mock is fully env-driven. Most commonly used:

- Networking:
  - `OXAIGEN_AUTH_MOCK_HOST` (default `127.0.0.1`)
  - `OXAIGEN_AUTH_MOCK_PORT` (default `8765`)
  - `OXAIGEN_AUTH_MOCK_RELOAD` (`1|true|yes` enables uvicorn reload)
  - `OXAIGEN_AUTH_MOCK_CORS_ORIGINS` (default `*`; use explicit origin list when needed)
- Tokens/cookies:
  - `OXAIGEN_AUTH_MOCK_TOKENS` (default includes `mock-dev-token`)
  - `OXAIGEN_AUTH_MOCK_ACCESS_TOKEN_VALUE` (default `mock-dev-token`)
  - `OXAIGEN_AUTH_MOCK_REFRESH_TOKEN_VALUE` (default `mock-dev-refresh-token`)
  - `OXAIGEN_AUTH_MOCK_ACCESS_COOKIE_NAME` (default `OxaigenPlatformAuthAccessToken`)
  - `OXAIGEN_AUTH_MOCK_REFRESH_COOKIE_NAME` (default `OxaigenPlatformAuthRefreshToken`)
  - `OXAIGEN_AUTH_MOCK_EXPIRES_IN` (default `300`)
  - `OXAIGEN_AUTH_MOCK_REFRESH_EXPIRES_IN` (default `1800`)
- OAuth flow behavior:
  - `OXAIGEN_AUTH_MOCK_RETURN_TO` (fallback return path, default `/`)
  - `OXAIGEN_AUTH_MOCK_AUTHORIZE_PATH` (default `/_oxa_auth/mock-authorize`)
  - `OXAIGEN_AUTH_MOCK_AUTH_CODE` (default `mock-auth-code`)
  - `OXAIGEN_AUTH_MOCK_LOGIN_AUTHORIZE_URL` (if set, overrides generated mock authorize URL)
- User/profile payload:
  - `OXAIGEN_AUTH_MOCK_EMAIL`, `OXAIGEN_AUTH_MOCK_USER_NAME`
  - `OXAIGEN_AUTH_MOCK_FIRST_NAME`, `OXAIGEN_AUTH_MOCK_LAST_NAME`
  - `OXAIGEN_AUTH_MOCK_USER_ID`, `OXAIGEN_AUTH_MOCK_ACCOUNT_TYPE`
  - `OXAIGEN_AUTH_MOCK_WORKSPACE_ID`, `OXAIGEN_AUTH_MOCK_WORKSPACE_NAME`
  - `OXAIGEN_AUTH_MOCK_ROLE_NAMES`
  - `OXAIGEN_AUTH_MOCK_PERMISSIONS`
  - `OXAIGEN_AUTH_MOCK_FORBIDDEN_TOKEN`

### Quick smoke test

```bash
# 1) Start mock
poetry run oxaigen-auth-mock

# 2) Check login URL generation
curl "http://127.0.0.1:8765/_oxa_auth/login?return_to=%2Fdashboard"

# 3) Inspect auth status
curl -X POST "http://127.0.0.1:8765/_oxa_auth/test-app-token"
```

## How it works

1. SDK reads `Authorization: Bearer <token>` from the request, or falls back
   to the `OxaigenPlatformAuthAccessToken` cookie.
2. SDK calls `/_oxa_auth/get-me` on the proxy (via `https://{request_host}`)
   with the token forwarded as `Authorization: Bearer`.
3. Proxy validates the token against Keycloak, looks up the user in the
   platform DB, returns a `MeResponse`.
4. SDK parses into a `User`, caches for 30s keyed on `sha256(token)`.
5. For `require_permission(name)`, SDK additionally calls
   `/_oxa_auth/test-app-permission/{name}`, also cached for 30s.

### Where the bearer token comes from

- **Same-origin frontend → backend** — the access cookie auto-attaches.
  The SDK reads it from the cookie.
- **Cross-origin frontend → backend** — frontend uses `useAccessToken()`
  from `@oxaigen/react` to read the JS-readable access cookie and forward
  it as `Authorization: Bearer`. The SDK reads the header.

### HTTP error mapping

Dependencies convert auth failures to FastAPI `HTTPException` responses:

- Missing/invalid token -> `401 Unauthorized`
- Missing permission -> `403 Forbidden`
- Auth proxy unavailable / upstream failure -> `502 Bad Gateway`

## Common pitfalls

- Adding middleware without using dependencies on routes: endpoints stay publicly accessible.
- Forgetting permission checks on privileged routes: use `require_permission(...)`.
- Host/proxy mismatch in local setups: configure `OXAIGEN_AUTH_PROXY_URL_OVERRIDE`.
- Using invalid permission names: only `[A-Za-z0-9_-]+` is accepted.

## User model deep dive

`User` is the SDK's typed representation of the auth proxy response (`/_oxa_auth/get-me`), scoped to one workspace and app context.

The model is designed for forward compatibility. If the proxy adds new fields, older SDK clients should keep working without an immediate SDK release.

### Why this model is permissive

The auth models intentionally use:

- Optional fields for most identity/profile values
- `extra = "allow"` on auth models

This gives app developers:

- Typed, stable access for known fields
- Non-breaking behavior when upstream adds fields
- Access to newly added proxy fields via `user.extra`

### Structure and semantics

#### Workspace-scoped models

- `UserWorkspace`: workspace identity in scope (`id`, `name`)
- `WorkspaceRole`: role assignment in the scoped workspace
- `WorkspacePermission`: permission assignment in the scoped workspace

All three allow unknown fields, so upstream schema expansion remains non-breaking.

#### Core identity fields

Common profile fields are optional because availability can vary by identity provider and rollout stage:

- `id`, `user_name`, `email`
- `account_type` (commonly `"internal"` or `"external"`)
- `enabled`, `first_name`, `last_name`

Treat these as "present when provided by upstream", not guaranteed invariants.

#### Authentication state

- `is_authenticated` defaults to `True` for resolved users
- `is_anonymous` is a convenience property (`not is_authenticated`)

In standard dependency-based usage, `get_current_user` returns authenticated users. Anonymous behavior is mainly relevant in optional/mixed-auth flows and tests.

#### Authorization data

- `workspace_roles` defaults to an empty list
- `workspace_permissions` defaults to an empty list

Using `default_factory=list` avoids mutable-default pitfalls and allows safe iteration even when upstream omits these fields.

## Middleware state contract

When `OxaigenAuthMiddleware` is installed, each request has:

- `request.state.user` (`User` or `None`)
- `request.state.auth_error` (`str` or `None`)

The middleware never blocks the request by itself. If upstream auth is down, it sets `auth_error` and continues; route dependencies decide enforcement.

## Advanced usage

Most apps only need dependencies + middleware. Advanced consumers can use:

- `ProxyAuthClient` for direct low-level proxy calls
- `extract_bearer_token` and `derive_proxy_base_url` for custom flows
- `aclose_client` to close SDK HTTP resources during app shutdown

## Production recommendations

- Keep `/health` public; protect business endpoints with dependencies.
- Use short timeout/cache defaults unless you have measured reasons to change.
- Add integration tests for:
  - unauthenticated access (expect failure),
  - authenticated access (expect success),
  - missing permission (expect failure),
  - required permission present (expect success).

## Deployment expectations

Your app must be served behind the Oxaigen proxy so `/_oxa_auth/*` endpoints are available on the same host as the frontend.

If those endpoints are unavailable, SDK auth checks cannot succeed.

---

## Local development setup (two-terminal flow)

When developing a backend app that uses the Oxaigen Auth SDK, you typically run **two processes side by side**:

1. The **auth mock server** — emulates the proxy's `/_oxa_auth/*` endpoints that the SDK calls to validate tokens.
2. Your **backend application** — the FastAPI app that imports the SDK.

This is necessary because in production, your app sits behind the Oxaigen proxy and the SDK resolves `/_oxa_auth/*` on the request's own `Host`. Locally there is no proxy in front, so the SDK needs to be told to call the mock instead. That's what `OXAIGEN_AUTH_DEV_MODE` is for.

### Terminal 1 — start the mock server

```bash
poetry run oxaigen-auth-mock
```

This boots the mock on `http://127.0.0.1:8765` and serves the full `/_oxa_auth/*` surface (login, callback, token-exchange, get-me, test-app-permission, etc.). Leave this running.

### Terminal 2 — run your backend with the SDK in dev mode

Set `OXAIGEN_AUTH_DEV_MODE=1` so the SDK routes validation calls to the mock instead of deriving the URL from the incoming request's `Host`:

```bash
export OXAIGEN_AUTH_DEV_MODE=1
# Optional — only set if your mock isn't on the default 127.0.0.1:8765
# export OXAIGEN_AUTH_DEV_PROXY_URL=http://127.0.0.1:8765

poetry run uvicorn my_app.main:app --reload --port 8000
```

Your backend now runs on `http://127.0.0.1:8000` and every SDK validation call (`/_oxa_auth/get-me`, `/_oxa_auth/test-app-permission/...`) goes to the mock on `127.0.0.1:8765`.

### How the SDK picks the auth URL

The SDK's `derive_proxy_base_url` resolves in this order:

1. **`OXAIGEN_AUTH_PROXY_URL_OVERRIDE`** — if set, used verbatim. Wins over everything, including dev mode. Useful for pointing at a fixed staging proxy.
2. **`OXAIGEN_AUTH_DEV_MODE=1`** — uses `OXAIGEN_AUTH_DEV_PROXY_URL` (default `http://127.0.0.1:8765`). This is the normal local-dev path.
3. **Default (production)** — builds `{scheme}://{host}` from the incoming request's `X-Forwarded-Host` or `Host` header, assuming the app is behind the Oxaigen proxy.

So in dev you generally only need `OXAIGEN_AUTH_DEV_MODE=1`; the default `OXAIGEN_AUTH_DEV_PROXY_URL` already matches the mock's default bind address.

### Sending requests against your dev backend

With the mock running, the default token `mock-dev-token` is valid. You can hit your backend directly:

```bash
curl -H "Authorization: Bearer mock-dev-token" http://127.0.0.1:8000/v1/me
```

The SDK will pull the bearer token off the header, call the mock at `http://127.0.0.1:8765/_oxa_auth/get-me`, and resolve a `User`. If you're driving a frontend against the mock, the access cookie set by the mock's callback flow will also be accepted by the SDK on same-origin requests.

### Tips

- If you change mock env vars (e.g. `OXAIGEN_AUTH_MOCK_PERMISSIONS`), restart Terminal 1 — the mock reads env at startup.
- Token validation results are cached for 30s by default. If you flip a token between valid and invalid while testing, either wait out the TTL or restart your backend.
- Running the mock on a non-default host/port? Set `OXAIGEN_AUTH_DEV_PROXY_URL` in Terminal 2 to match (e.g. `http://127.0.0.1:9000`), and also set `OXAIGEN_AUTH_MOCK_HOST`/`OXAIGEN_AUTH_MOCK_PORT` in Terminal 1.
- To temporarily point dev traffic at a remote staging proxy instead of the local mock, unset `OXAIGEN_AUTH_DEV_MODE` and set `OXAIGEN_AUTH_PROXY_URL_OVERRIDE=https://your-staging-host` — no other changes needed.
