# remem-auth

Shared authentication library for remem Python services. Supports JWT verification (Azure Entra ID / Google) and static bearer tokens, with out-of-the-box integrations for FastAPI and FastMCP.

## Installation

```bash
# Core library (JWT verification + static tokens)
pip install remem-auth

# With FastAPI integration
pip install remem-auth[fastapi]

# With FastMCP integration
pip install remem-auth[fastmcp]

# All optional dependencies
pip install remem-auth[all]
```

Requires Python 3.11+.

## Quick Start

### 1. Configure Environment Variables

All settings are read from environment variables with the `REMEM_AUTH_` prefix.

As a library, remem-auth does **not** load `.env` files by default — the environment variable loading strategy is the consumer's responsibility. Common approaches:

```python
# Option 1: Use load_dotenv() in your app's entrypoint
from dotenv import load_dotenv
load_dotenv()
config = AuthConfig()

# Option 2: Explicitly point AuthConfig to a .env file
config = AuthConfig(_env_file=".env")

# Option 3: Rely on system/container-injected env vars (no .env needed)
config = AuthConfig()
```

#### Available Variables

| Variable | Description | Default |
|---|---|---|
| `REMEM_AUTH_AZURE_TENANT_ID` | Azure Entra ID tenant ID | `""` |
| `REMEM_AUTH_AZURE_CLIENT_ID` | Azure app registration client ID | `""` |
| `REMEM_AUTH_GOOGLE_CLIENT_ID` | Google OAuth client ID | `""` |
| `REMEM_AUTH_STATIC_TOKENS` | Comma-separated static bearer tokens | `""` |
| `REMEM_AUTH_IDPS_JSON` | Advanced: full JSON array of IdP configs | `""` |
| `REMEM_AUTH_VERIFY_EXP` | Whether to verify token expiration | `True` |
| `REMEM_AUTH_VERIFY_AUD` | Whether to verify audience | `True` |

**Example — Azure Entra ID:**

```bash
export REMEM_AUTH_AZURE_TENANT_ID="your-tenant-id"
export REMEM_AUTH_AZURE_CLIENT_ID="your-client-id"
```

**Example — Static tokens (useful for dev/test):**

```bash
export REMEM_AUTH_STATIC_TOKENS="dev-token-1,dev-token-2"
```

**Example — Custom IdP via JSON:**

```bash
export REMEM_AUTH_IDPS_JSON='[{"name":"my-idp","issuer":"https://idp.example.com","jwks_uri":"https://idp.example.com/.well-known/jwks.json","audience":"my-app"}]'
```

### 2. FastAPI Integration

```python
from fastapi import Depends, FastAPI
from remem.auth import AuthConfig, AuthenticatedUser, FastAPIAuth

config = AuthConfig()  # reads from environment variables
auth = FastAPIAuth(config)
app = FastAPI()

@app.get("/protected")
def protected(user: AuthenticatedUser = Depends(auth)):
    return {"subject": user.subject, "email": user.email}
```

Unauthenticated requests receive a `401 Unauthorized` response with a `WWW-Authenticate: Bearer` header.

**OpenAI-compatible error format:**

For services that need to return errors in the [OpenAI error format](https://platform.openai.com/docs/guides/error-codes), pass `error_format="openai"` and call `install()` on the app:

```python
from fastapi import Depends, FastAPI
from remem.auth import AuthConfig, AuthenticatedUser, ErrorFormat, FastAPIAuth

config = AuthConfig()
auth = FastAPIAuth(config, error_format="openai")
app = FastAPI()
auth.install(app)  # registers the OpenAI error response handler

@app.get("/protected")
def protected(user: AuthenticatedUser = Depends(auth)):
    return {"subject": user.subject}
```

Authentication failures will return:

```json
{
  "error": {
    "message": "No token provided",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_api_key"
  }
}
```

If `install()` is not called, the error gracefully falls back to the default `{"detail": "..."}` format.

### 3. FastMCP Integration

```python
from fastmcp import FastMCP
from remem.auth import AuthConfig, FastMCPAuthProvider

config = AuthConfig()
mcp = FastMCP("my-server", auth=FastMCPAuthProvider(config))

@mcp.tool()
def hello() -> str:
    return "world"
```

FastMCPAuthProvider implements FastMCP's `TokenVerifier` interface and uses `asyncio.to_thread()` internally so the synchronous verifier doesn't block the event loop.

### 4. Using the Core API Directly

If you're not using either framework, you can call the verification engine directly:

```python
from remem.auth import AuthConfig, AuthVerifier, AuthenticationError

config = AuthConfig()
verifier = AuthVerifier(config)

try:
    user = verifier.verify_token("eyJhbGciOi...")
    print(user.subject, user.email, user.auth_method)
except AuthenticationError as e:
    print(f"Authentication failed: {e}")
```

**Extracting tokens from request headers:**

```python
from remem.auth import extract_token

# Works with any object that has a .headers attribute (Mapping[str, str])
token = extract_token(request)
```

`extract_token` checks `Authorization: Bearer <token>` first, then falls back to the `api-key` header.

## Core Concepts

### Verification Flow

`AuthVerifier.verify_token()` processes tokens in this order:

1. **Auth not enabled** — raises `AuthenticationError` (auth configuration is required)
2. **No token** — raises `AuthenticationError`
3. **Matches a static token** — returns a static-token user (O(1) set lookup)
4. **JWT** — peeks the issuer from an unverified decode, then routes to the matching IdP verifier

### AuthenticatedUser

The user object returned after successful authentication:

```python
class AuthenticatedUser(BaseModel):
    subject: str                     # JWT "sub" claim
    email: str | None                # extracted from email / preferred_username / upn
    name: str | None                 # JWT "name" claim
    auth_method: AuthMethod          # "jwt" | "static" | "none"
    idp_name: str | None             # IdP name (e.g. "azure", "google")
    claims: dict[str, Any]           # full JWT payload
    token: str                       # raw token (excluded from serialization)
```

The `token` field is marked with `exclude=True` and `repr=False`, so it won't appear in `.model_dump()` output or `print()` — preventing accidental credential leaks.

### Auth Required

When no IdPs or static tokens are configured, `auth_enabled` is `False` and all requests are **rejected** with an `AuthenticationError`. This ensures authentication is always explicitly configured before a service accepts requests.

## API Reference

### Module Exports

```python
from remem.auth import (
    AuthConfig,                # pydantic-settings configuration
    AuthVerifier,              # core verification engine
    AuthenticationError,       # verification failure exception
    AuthenticatedUser,         # user model
    AuthMethod,                # authentication method enum
    ErrorFormat,               # error response format enum
    IdpConfig,                 # identity provider config model
    extract_token,             # extract token from request headers
    create_auth_config_from_env,  # AuthConfig factory function
    FastAPIAuth,               # FastAPI dependency (lazy import)
    FastMCPAuthProvider,       # FastMCP auth provider (lazy import)
)
```

`FastAPIAuth` and `FastMCPAuthProvider` are lazy-imported via `__getattr__` — their framework dependencies are only loaded when actually accessed.

### IdpConfig Fields

When using `REMEM_AUTH_IDPS_JSON` to define custom IdPs, each entry supports:

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `name` | `str` | Yes | — | IdP identifier |
| `issuer` | `str` | Yes | — | JWT issuer (used for routing) |
| `jwks_uri` | `str` | Yes | — | JWKS endpoint URL |
| `audience` | `str \| None` | No | `None` | Expected audience |
| `algorithms` | `list[str]` | No | `["RS256"]` | Allowed signing algorithms |

## Development

```bash
# Create a virtualenv and install dev dependencies
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

# Run tests
pytest tests/ -v

# Lint
ruff check src/ tests/
```
