modo-auth 0.1.0

Authentication framework for modo
Documentation
# modo-auth

Session-based authentication and Argon2id password hashing for modo applications.

## Features

| Feature     | What it enables                                                                           |
| ----------- | ----------------------------------------------------------------------------------------- |
| `templates` | `UserContextLayer` — injects the authenticated user into the minijinja template context |

## Key Types

| Type                     | Purpose                                                                    |
| ------------------------ | -------------------------------------------------------------------------- |
| `UserProvider`           | Trait — implement on your user repository to load users by session ID      |
| `UserProviderService<U>` | Type-erased wrapper around a `UserProvider`; register with `app.service()` |
| `Auth<U>`                | Extractor — requires an authenticated user; returns 401 if absent          |
| `OptionalAuth<U>`        | Extractor — resolves user if present, yields `None` if not authenticated   |
| `PasswordHasher`         | Argon2id hashing service with `hash_password` / `verify_password`          |
| `PasswordConfig`         | Argon2id tuning knobs (memory, iterations, parallelism)                    |
| `UserContextLayer`       | Tower layer (feature `templates`) — injects user into template context     |

## Usage

### 1. Implement `UserProvider`

```rust
use modo_auth::{UserProvider, UserProviderService};

#[derive(Clone)]
struct MyUser {
    id: String,
    name: String,
}

struct UserRepo {
    // db pool, etc.
}

impl UserProvider for UserRepo {
    type User = MyUser;

    async fn find_by_id(&self, id: &str) -> Result<Option<MyUser>, modo::Error> {
        // load from your database
        todo!()
    }
}
```

### 2. Register services in `main`

```rust
use modo_auth::{UserProviderService, PasswordHasher};

#[modo::main]
async fn main(
    app: modo::app::AppBuilder,
    config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
    let repo = UserRepo { /* ... */ };
    let hasher = PasswordHasher::default();

    app.service(UserProviderService::new(repo))
       .service(hasher)
       .layer(modo_session::layer(session_store))
       .run()
       .await
}
```

### 3. Use extractors in handlers

```rust
use modo_auth::{Auth, OptionalAuth};

// Requires authentication — returns 401 if no session / user not found
async fn profile(Auth(user): Auth<MyUser>) -> String {
    format!("Hello, {}", user.name)
}

// Optional — never rejects, yields None when not authenticated
async fn home(OptionalAuth(user): OptionalAuth<MyUser>) -> String {
    match user {
        Some(u) => format!("Welcome back, {}", u.name),
        None => "Welcome, guest".to_string(),
    }
}
```

### 4. Hash and verify passwords

```rust
use modo_auth::{PasswordHasher, PasswordConfig};
use modo::extractors::Service;

// Use default OWASP-recommended settings
let hasher = PasswordHasher::default();

let hash = hasher.hash_password("correct-horse-battery-staple").await?;
let valid = hasher.verify_password("correct-horse-battery-staple", &hash).await?;

// Extract in a handler
async fn register(
    Service(hasher): Service<PasswordHasher>,
    // ...
) {
    let hash = hasher.hash_password(&form.password).await?;
    // store hash in DB
}
```

### 5. Custom Argon2id parameters

```rust
use modo_auth::{PasswordConfig, PasswordHasher};

let config = PasswordConfig {
    memory_cost_kib: 32768, // 32 MiB
    time_cost: 3,
    parallelism: 1,
};
let hasher = PasswordHasher::new(config)?;
```

`PasswordConfig` implements `serde::Deserialize` with `#[serde(default)]`, so you can load it from YAML with partial overrides:

```yaml
password:
    memory_cost_kib: 32768
    # time_cost and parallelism fall back to defaults (2 and 1)
```

### 6. Inject user into template context (feature `templates`)

```rust
use modo_auth::{UserContextLayer, UserProviderService};

// In main — add after the session layer
app.service(UserProviderService::new(repo))
   .layer(UserContextLayer::new(UserProviderService::new(repo2)))
   .layer(modo_session::layer(session_store))
   .run()
   .await
```

The layer inserts the authenticated user as `"user"` into the minijinja `TemplateContext`, available in every template without explicit handler code. If no session exists or the user is not found, nothing is injected.

## Error Behaviour

| Condition                               | `Auth<U>` | `OptionalAuth<U>` |
| --------------------------------------- | --------- | ----------------- |
| No session                              | 401       | `None`            |
| Session present, user not found         | 401       | `None`            |
| Provider returns `Err`                  | 500       | 500               |
| Session middleware not registered       | 500       | 500               |
| `UserProviderService<U>` not registered | 500       | 500               |