Metadata-Version: 2.5
Name: csrd-auth
Version: 0.7.0
Summary: Pluggable JWT authentication — authenticators, key providers, and FastAPI dependency factories
Project-URL: Repository, https://github.com/csrd-api/fastapi-common
Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/auth
Project-URL: Changelog, https://github.com/csrd-api/fastapi-common/blob/main/CHANGELOG.md
License: MIT
Requires-Python: >=3.12
Requires-Dist: cryptography>=43
Requires-Dist: csrd-context
Requires-Dist: csrd-logging
Requires-Dist: csrd-models
Requires-Dist: fastapi<1,>=0.115
Requires-Dist: pydantic<3,>=2
Requires-Dist: pyjwt[crypto]<3,>=2
Description-Content-Type: text/markdown

# csrd-auth

Pluggable JWT authentication for FastAPI — authenticators, key providers, and dependency factories.

**Tier 2 (bridge)** — depends on `csrd-models` and `csrd-context` only.

## Installation

```bash
pip install "csrd-auth @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/auth"
```

## Usage

```python
from csrd.auth import (
    JWTAuthenticator,
    StaticKeyProvider,
    create_bearer_dependency,
    create_jwt_bearer,
)
```

### Quick start — JWT bearer dependency

```python
# Standalone (provide your own token_finder)
from csrd.auth import create_jwt_bearer

auth_dep = create_jwt_bearer(
    key="your-secret",
    token_finder=lambda: extract_token_from_request(),
)
```

When used with `csrd.versioning`, wire in `find_token` explicitly:

```python
from csrd.auth import create_jwt_bearer
from csrd.versioning import find_token

auth_dep = create_jwt_bearer(key="your-secret", token_finder=find_token)
```

## Internal token guard (service-to-service)

`require_internal_token` protects endpoints that other services call (not
users) with a shared token compared in constant time. It fails closed: when
the configured token is unset or empty, every request gets 403.

```python
from typing import Annotated

from fastapi import Depends

from csrd.auth import require_internal_token
from csrd.models import BaseSettings, InternalToken


class Settings(BaseSettings):
    internal_token: InternalToken = ""  # refuses to boot unless INTERNAL_TOKEN is a real secret


settings = Settings()

# Defaults: header "X-Internal-Token", setting "internal_token"
Internal = Annotated[None, Depends(require_internal_token(settings))]


@router.post("/internal/refs")
async def refs(_: Internal) -> dict: ...
```

Custom names (e.g. `X-Internal-Service-Token` / `internal_service_token`):

```python
InternalService = Annotated[
    None,
    Depends(
        require_internal_token(
            settings,
            setting_name="internal_service_token",
            header="X-Internal-Service-Token",
        )
    ),
]
```

- `settings` is the settings object or a zero-arg provider (e.g. a cached
  `get_settings`), resolved on every request. `str` and `SecretStr` values work.
- Rejection raises `HTTPException(403, "Not allowed")` by default. csrd-auth
  cannot depend on csrd-service (tier rules), so to render through
  `service_exception_handler` pass
  `error_factory=lambda: AuthorizationError("Not allowed")`.
- Callers send the header with `BaseDelegate(..., internal_token=settings.internal_token)`.
- Strip the header from inbound client traffic at the gateway.

## Authenticators

| Class | Strategy |
|-------|----------|
| `JWTAuthenticator` | Decode & verify JWT locally via PyJWT |
| `StaticAuthenticator` | Accept a fixed token (dev/testing) |
| `RemoteAuthenticator` | Validate via upstream HTTP endpoint |
| `CallbackAuthenticator` | Delegate to any sync/async callable |
| `ChainedAuthenticator` | Try multiple authenticators in order |

## Key Providers

| Class | Strategy |
|-------|----------|
| `StaticKeyProvider` | Fixed string, bytes, or `SecretStr` |
| `EnvKeyProvider` | Read from `app.state` or settings loader |
| `JWKSKeyProvider` | Fetch from JWKS endpoint (cached with TTL) |
| `MultiKeyProvider` | Route by `kid` header or try all |
| `CallbackKeyProvider` | Delegate to any callable |

## Protocols

- `Authenticator` — `(Request, str) -> UserClaims`
- `KeyProvider` — `(token_headers, app) -> key`

Both are `@runtime_checkable` protocols.

## Dependency Tier

```
Tier 1      csrd.models  csrd.lifespan  csrd.context
Tier 2      csrd.auth  csrd.logging  csrd.delegate  csrd.repository  csrd.service
Tier 3      csrd.versioning
```

`csrd.auth` depends only on `csrd.models` (for `UserClaims`) and `csrd.context` (for `user_info_context`). It has **no dependency** on `csrd.versioning`.
