# vikdata-auth

Standalone JWT validation SDK for the vikdata platform. This SDK provides JWT validation via JWKS with no database dependencies, following the industry-standard approach used by Google, AWS, and Azure.

## Features

- **JWKS Fetching**: Automatic JWKS fetching with intelligent caching (1 hour TTL)
- **JWT Validation**: RS256 JWT validation with automatic key rotation support
- **Permission Matching**: Wildcard-enabled permission matching (`vessels:*`, `*:read`, `*:*`)
- **FastAPI Integration**: Middleware and decorators for FastAPI applications
- **Zero Database Dependencies**: Pure HTTP + crypto - no database required

## Installation

```bash
pip install vikdata-auth

# With FastAPI support
pip install vikdata-auth[fastapi]
```

## Quick Start

### Basic JWT Validation

```python
from vikdata_auth import JWTValidator, has_permission

# Create validator (typically once at startup)
validator = JWTValidator(
    jwks_url="https://api.vikdata.com/.well-known/jwks.json",
)

# Validate a token
claims = await validator.validate(token)
print(f"User: {claims.user_id}")
print(f"Tenant: {claims.tenant_id}")

# Check permissions
if has_permission(claims.permissions, "vessels:read"):
    print("User can read vessels")
```

### FastAPI Middleware

```python
from fastapi import FastAPI, Request
from vikdata_auth import JWTValidationMiddleware

app = FastAPI()

# Add middleware - validates all requests except excluded paths
app.add_middleware(
    JWTValidationMiddleware,
    jwks_url="https://api.vikdata.com/.well-known/jwks.json",
    exclude_paths=["/health", "/metrics"],
    exclude_prefixes=["/docs", "/openapi.json"],
)

@app.get("/api/v1/vessels")
async def list_vessels(request: Request):
    # request.state populated by middleware
    tenant_id = request.state.tenant_id
    user_id = request.state.user_id
    permissions = request.state.permissions
    return {"vessels": [...]}
```

### FastAPI Decorators

```python
from fastapi import APIRouter, Depends
from vikdata_auth import require_permission, require_role

router = APIRouter()

@router.get("/vessels")
async def list_vessels(
    _: None = require_permission("vessels:read"),
):
    return {"vessels": [...]}

@router.post("/vessels")
async def create_vessel(
    data: VesselCreate,
    _: None = require_permission("vessels:write"),
):
    # Create vessel...
    pass

@router.delete("/admin/users/{id}")
async def delete_user(
    id: str,
    _: None = require_role("admin"),
):
    # Delete user...
    pass
```

### Permission Matching

```python
from vikdata_auth import has_permission, has_any_permission, has_all_permissions

# Single permission check
if has_permission(["vessels:read", "vessels:write"], "vessels:read"):
    print("Can read vessels")

# Wildcard matching
if has_permission(["vessels:*"], "vessels:delete"):
    print("Can delete vessels (wildcard)")

if has_permission(["*:*"], "anything:here"):
    print("Super admin access")

# Multiple permissions
if has_any_permission(perms, ["admin:read", "vessels:read"]):
    print("Has at least one permission")

if has_all_permissions(perms, ["vessels:read", "vessels:write"]):
    print("Has all required permissions")
```

### Rate Limiting

```python
from vikdata_auth import rate_limit

@router.get("/api/v1/search")
async def search(
    _: None = rate_limit(requests=10, window=60),  # 10 requests per minute
):
    return {"results": [...]}
```

## API Reference

### Core Classes

#### `JWTValidator`

Validates JWTs using JWKS from vikdata.

```python
validator = JWTValidator(
    jwks_url="https://api.vikdata.com/.well-known/jwks.json",
    audience="vikdata",  # Expected audience
    issuer="https://api.vikdata.com",  # Expected issuer
    algorithms=["RS256"],  # Allowed algorithms
    cache_ttl=3600,  # JWKS cache TTL in seconds
)

claims = await validator.validate(token)
```

#### `TokenClaims`

Validated JWT claims dataclass.

```python
@dataclass
class TokenClaims:
    sub: str  # Subject (user ID or API key ID)
    tenant_id: str
    email: str
    name: str | None
    permissions: list[str]
    roles: list[str]
    exp: int  # Expiration timestamp
    iat: int  # Issued-at timestamp
    iss: str  # Issuer
    aud: str | list[str]  # Audience
    jti: str | None  # JWT ID
    entity_type: str  # "user" or "api_key"
```

Properties:
- `user_id` - Alias for `sub`
- `is_user` - True if entity_type is "user"
- `is_api_key` - True if entity_type is "api_key"

### Permission Functions

- `match_permission(user_permission, required)` - Check if single permission matches
- `has_permission(permissions, required)` - Check if any permission matches
- `has_any_permission(permissions, required_list)` - Check if any of multiple required
- `has_all_permissions(permissions, required_list)` - Check if all required

### FastAPI Decorators

- `require_permission(permission)` - Require specific permission
- `require_any_permission(*permissions)` - Require any of the permissions
- `require_all_permissions(*permissions)` - Require all permissions
- `require_role(role)` - Require specific role
- `require_any_role(*roles)` - Require any of the roles
- `require_tenant_match(param="tenant_id")` - Require path param matches user's tenant
- `rate_limit(requests, window, key_func, limiter)` - Rate limit requests

### Exceptions

- `VikdataAuthError` - Base exception
- `AuthenticationError` - Invalid or missing credentials
- `AuthorizationError` - Permission denied
- `JWKSError` - Error fetching or parsing JWKS

## Development

```bash
# Install dev dependencies
pip install -e ".[dev,fastapi]"

# Run tests
pytest

# Type check
mypy src/vikdata_auth

# Lint
ruff check src/vikdata_auth
```

## License

MIT
