# @psyche/auth

Authentication library for Psyche Python services, aligned with
`@oshun/auth-primitives`.

## Features

- **JWT Service** - Token signing, verification, and decoding
- **Session Management** - In-memory and Redis session stores
- **API Key Management** - Generation, validation, and storage
- **Password Utilities** - Secure hashing with Argon2/bcrypt/PBKDF2
- **FastAPI Middleware** - Ready-to-use authentication dependencies

## Installation

```bash
# Core package
poetry add psyche-auth

# With all optional dependencies
poetry add "psyche-auth[all]"

# Specific extras
poetry add "psyche-auth[argon2]"  # Argon2 password hashing
poetry add "psyche-auth[bcrypt]"   # bcrypt password hashing
poetry add "psyche-auth[redis]"    # Redis session store
```

## Quick Start

### JWT Authentication

```python
from psyche_auth import (
    JwtService,
    JwtConfig,
    JwtPayload,
    KeyConfig,
    KeyAlgorithm,
)

# Create JWT service
jwt_service = JwtService(
    JwtConfig(
        issuer="psyche",
        audience="psyche",
        access_token_ttl=900,  # 15 minutes
        refresh_token_ttl=604800,  # 7 days
        key=KeyConfig(
            algorithm=KeyAlgorithm.HS256,
            secret="your-secret-key",
        ),
    )
)

# Sign a token
token = await jwt_service.sign(
    JwtPayload(user_id="123", email="user@example.com", roles=["user"])
)

# Verify a token
result = await jwt_service.verify(token)
if result.valid:
    print(f"User ID: {result.payload.user_id}")
else:
    print(f"Error: {result.error}")

# Create token pair (access + refresh)
token_pair = await jwt_service.create_token_pair(
    JwtPayload(user_id="123", roles=["user"])
)
print(f"Access Token: {token_pair.access_token}")
print(f"Refresh Token: {token_pair.refresh_token}")
```

### FastAPI Integration

```python
from fastapi import Depends, FastAPI
from psyche_auth import (
    AuthContext,
    AuthDependency,
    create_hmac_jwt_service,
)

app = FastAPI()

# Create JWT service
jwt_service = create_hmac_jwt_service(
    secret="your-secret-key",
    issuer="psyche",
)

# Create auth dependency
auth = AuthDependency(jwt_service)


@app.get("/protected")
async def protected_route(context: AuthContext = Depends(auth.required)):
    return {"user_id": context.user_id, "roles": context.roles}


@app.get("/optional")
async def optional_route(context: AuthContext = Depends(auth.optional)):
    if context.is_authenticated:
        return {"user_id": context.user_id}
    return {"user_id": None}


@app.get("/admin")
async def admin_route(
    context: AuthContext = Depends(auth.with_roles("admin"))
):
    return {"admin": True}
```

### Session Management

```python
from psyche_auth import (
    InMemorySessionStore,
    RedisSessionStore,
    SessionConfig,
    SessionOptions,
)

# In-memory store (development)
store = InMemorySessionStore(
    SessionConfig(
        ttl=86400,  # 24 hours
        rolling=True,  # Extend TTL on activity
    )
)

# Create session
session = await store.create(
    user_id="123",
    data={"preferences": {"theme": "dark"}},
    options=SessionOptions(
        ip_address="192.168.1.1",
        user_agent="Mozilla/5.0...",
    ),
)

# Get session
session = await store.get(session.id)

# Update session
await store.update(session.id, {"last_action": "view_dashboard"})

# Delete session
await store.delete(session.id)

# Redis store (production)
import redis.asyncio as redis

redis_client = redis.from_url("redis://localhost:6379")
redis_store = RedisSessionStore(redis_client)
```

### API Keys

```python
from psyche_auth import (
    InMemoryApiKeyStore,
    ApiKeyDependency,
)

# Create store
store = InMemoryApiKeyStore()

# Generate API key
api_key, full_key = await store.create(
    name="Production API Key",
    owner_id="user_123",
    owner_type="user",
    scopes=["read", "write"],
)

print(f"API Key (save this!): {full_key}")
print(f"Key Prefix: {api_key.key_prefix}")

# Validate API key
validated = await store.validate(full_key)
if validated:
    print(f"Valid! Scopes: {validated.scopes}")

# FastAPI integration
api_key_auth = ApiKeyDependency(store)

@app.get("/api/data")
async def get_data(api_key = Depends(api_key_auth.required)):
    return {"owner": api_key.owner_id}
```

### Password Hashing

```python
from psyche_auth import (
    hash_password,
    verify_password,
    validate_password,
    generate_password,
    PasswordPolicy,
)

# Hash a password
result = hash_password("mypassword123")
print(f"Hash: {result.hash}")
print(f"Algorithm: {result.algorithm}")

# Verify a password
is_valid = verify_password("mypassword123", result)
print(f"Valid: {is_valid}")

# Validate password against policy
validation = validate_password(
    "weak",
    PasswordPolicy(
        min_length=8,
        require_uppercase=True,
        require_numbers=True,
    ),
)
print(f"Valid: {validation.valid}")
print(f"Errors: {validation.errors}")

# Generate a secure password
password = generate_password(
    length=16,
    include_special=True,
)
print(f"Generated: {password}")
```

## Configuration

### Environment Variables

```bash
# JWT Configuration
JWT_SECRET=your-secret-key
JWT_ISSUER=psyche
JWT_AUDIENCE=psyche
JWT_ACCESS_TOKEN_TTL=900
JWT_REFRESH_TOKEN_TTL=604800

# Redis (for session store)
REDIS_URL=redis://localhost:6379
```

### Pydantic Settings

```python
from pydantic_settings import BaseSettings
from psyche_auth import JwtConfig, KeyConfig, KeyAlgorithm


class AuthSettings(BaseSettings):
    jwt_secret: str
    jwt_issuer: str = "psyche"
    jwt_audience: str = "psyche"
    jwt_access_token_ttl: int = 900
    jwt_refresh_token_ttl: int = 604800

    class Config:
        env_prefix = "JWT_"

    def get_jwt_config(self) -> JwtConfig:
        return JwtConfig(
            issuer=self.jwt_issuer,
            audience=self.jwt_audience,
            access_token_ttl=self.jwt_access_token_ttl,
            refresh_token_ttl=self.jwt_refresh_token_ttl,
            key=KeyConfig(
                algorithm=KeyAlgorithm.HS256,
                secret=self.jwt_secret,
            ),
        )
```

## Alignment with @oshun/auth-primitives

This library provides Python equivalents for all key types and interfaces:

| TypeScript           | Python                 |
| -------------------- | ---------------------- |
| `JwtPayload`         | `JwtPayload`           |
| `JwtConfig`          | `JwtConfig`            |
| `VerificationResult` | `VerificationResult`   |
| `TokenPair`          | `TokenPair`            |
| `Session`            | `Session`              |
| `SessionStore`       | `InMemorySessionStore` |
| `ApiKey`             | `ApiKey`               |
| `AuthContext`        | `AuthContext`          |
| `PasswordPolicy`     | `PasswordPolicy`       |

## Nx Targets

| Target         | Description             |
| -------------- | ----------------------- |
| `build`        | Build the package       |
| `install`      | Install dependencies    |
| `lint`         | Run Ruff linter         |
| `lint-fix`     | Fix linting issues      |
| `format`       | Format with Black       |
| `format-check` | Check formatting        |
| `typecheck`    | Run MyPy                |
| `test`         | Run pytest              |
| `test-cov`     | Run tests with coverage |

## License

Proprietary - Oshun Platform
