modo-rs 0.6.2

Rust web framework for small monolithic apps
Documentation
# modo::auth

Authentication utilities for the modo framework: Argon2id password hashing,
numeric OTP, RFC 6238 TOTP, backup recovery codes, JWT middleware, and OAuth 2.0
provider integrations (GitHub, Google).

## Feature Flag

All items in this module require the `auth` feature:

```toml
[dependencies]
modo = { version = "0.6", features = ["auth"] }
```

## Modules

| Module     | Purpose                                                    |
| ---------- | ---------------------------------------------------------- |
| `password` | Argon2id password hashing and verification                 |
| `otp`      | Numeric one-time password generation and verification      |
| `totp`     | RFC 6238 TOTP (Google Authenticator compatible)            |
| `backup`   | One-time backup recovery code generation and verification  |
| `jwt`      | JWT encoding, decoding, signing, and axum Tower middleware |
| `oauth`    | OAuth 2.0 provider integrations (GitHub, Google)           |

## Usage

### Password Hashing

`PasswordConfig` holds Argon2id parameters with OWASP-recommended defaults
(19 MiB memory, 2 iterations, 1 thread, 32-byte output). `hash` and `verify`
run on blocking threads so they do not starve the async runtime. The returned
string is PHC-formatted and embeds the algorithm, parameters, and salt.

```rust
use modo::auth::password::{self, PasswordConfig};

let config = PasswordConfig::default();

// Hash on registration
let hash = password::hash("hunter2", &config).await?;

// Verify on login — returns false for wrong password, Err only for
// a structurally invalid hash string
let ok = password::verify("hunter2", &hash).await?;
assert!(ok);
```

### One-Time Password (OTP)

Generates a numeric code of the requested length using rejection sampling to
avoid modulo bias. Store only the hash; send the plaintext to the user.
Comparison is constant-time.

```rust
use modo::auth::otp;

// Generate a 6-digit code
let (code, hash) = otp::generate(6);
// store `hash` in the database, send `code` to the user via email or SMS

// Verify the submitted code
let ok = otp::verify(&submitted_code, &stored_hash);
```

### TOTP (Authenticator App)

Compatible with Google Authenticator, Authy, and any RFC 6238 authenticator.
The secret is stored as base32. `verify` accepts codes within ±`window` time
steps of the current step.

```rust
use modo::auth::totp::{Totp, TotpConfig};

// Provisioning: generate a secret and QR code URI
let secret = Totp::generate_secret(); // base32-encoded, store in DB
let config = TotpConfig::default();   // 6 digits, 30s step, ±1 window
let totp = Totp::from_base32(&secret, &config)?;
let uri = totp.otpauth_uri("MyApp", "user@example.com");
// render `uri` as a QR code for the user to scan

// Verification on every login
let totp = Totp::from_base32(&stored_secret, &config)?;
let ok = totp.verify(&submitted_code);
```

### Backup Recovery Codes

Generates alphanumeric `xxxx-xxxx` codes using rejection sampling. Display the
plaintext to the user once; store only the hashes. The verifier normalizes input
(strips hyphens, lowercases) before comparing.

```rust
use modo::auth::backup;

// Generate 10 codes on TOTP enrollment
let codes = backup::generate(10);
// codes: Vec<(plaintext_code, sha256_hex_hash)>
// store the hashes, show the plaintext once

// Verify a submitted recovery code (accepts with or without hyphen separator)
let ok = backup::verify(&submitted_code, &stored_hash);
```

### JWT Middleware

`JwtEncoder` signs tokens, `JwtDecoder` verifies them, and `JwtLayer` is a
Tower middleware that enforces authentication on axum routes. `Claims<T>` is
the extractor for handlers once the middleware is in place.

`exp` is always required by `JwtDecoder`. When `default_expiry` is set in
`JwtConfig`, the encoder auto-fills `exp` when the caller does not set it.

```rust
use modo::auth::jwt::{Claims, JwtConfig, JwtDecoder, JwtEncoder, JwtLayer};
use serde::{Deserialize, Serialize};
use axum::{Router, routing::get};

#[derive(Clone, Serialize, Deserialize)]
struct MyClaims {
    role: String,
}

// Build encoder and decoder from the same config
let mut config = JwtConfig::new("change-me-in-production");
config.default_expiry = Some(3600);
config.leeway = 5;
let encoder = JwtEncoder::from_config(&config);
let decoder = JwtDecoder::from_config(&config);

// Issue a token (exp is auto-filled from default_expiry)
let claims = Claims::new(MyClaims { role: "admin".into() })
    .with_sub("user_01ABC");
let token = encoder.encode(&claims)?;

// Wire the middleware — protects all routes in this router
let app: Router = Router::new()
    .route("/me", get(me_handler))
    .layer(JwtLayer::<MyClaims>::new(decoder));

// Handler — Claims<T> is extracted from request extensions set by JwtLayer
async fn me_handler(claims: Claims<MyClaims>) -> String {
    format!("hello {}", claims.sub.unwrap_or_default())
}
```

#### Custom Token Sources

By default `JwtLayer` reads `Authorization: Bearer <token>`. Override this
with any combination of `BearerSource`, `CookieSource`, `QuerySource`, or
`HeaderSource`, or your own `TokenSource` implementation:

```rust
use modo::auth::jwt::{JwtLayer, TokenSource};
use modo::auth::jwt::{BearerSource, CookieSource};
use std::sync::Arc;

let layer = JwtLayer::<MyClaims>::new(decoder)
    .with_sources(vec![
        Arc::new(BearerSource) as Arc<dyn TokenSource>,
        Arc::new(CookieSource("jwt")) as Arc<dyn TokenSource>,
    ]);
```

#### Token Revocation

Implement `Revocation` against any async store (database, cache, Redis, etc.)
and attach it to the layer. Tokens with a `jti` claim are checked on every
request; tokens without `jti` are accepted without a revocation check.
The middleware is fail-closed — a backend error rejects the request.

```rust
use modo::auth::jwt::{JwtLayer, Revocation};
use modo::Result;
use std::pin::Pin;
use std::sync::Arc;

struct MyRevocationStore;

impl Revocation for MyRevocationStore {
    fn is_revoked(&self, jti: &str) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + '_>> {
        let jti = jti.to_string();
        Box::pin(async move {
            // query your DB or cache here
            Ok(false)
        })
    }
}

let layer = JwtLayer::<MyClaims>::new(decoder)
    .with_revocation(Arc::new(MyRevocationStore));
```

### OAuth 2.0

The OAuth module provides the Authorization Code + PKCE flow for Google and
GitHub. `OAuthProvider::authorize_url` produces an `AuthorizationRequest` that
redirects the user and sets a signed `_oauth_state` cookie. On the callback
route, extract `OAuthState` (reads and verifies the cookie) and
`CallbackParams` (the `?code=&state=` query params), then call
`OAuthProvider::exchange` to obtain a `UserProfile`.

A `Key` (from `axum_extra::extract::cookie::Key`) must be registered in the
`Registry` for cookie signing. Both `Google` and `GitHub` also require a
`CookieConfig` for cookie attributes.

```rust
use modo::auth::oauth::{
    GitHub, Google, OAuthConfig, OAuthProviderConfig, OAuthProvider,
    OAuthState, CallbackParams, UserProfile,
};
use modo::auth::oauth::AuthorizationRequest;

// Build from config (typically loaded from YAML)
let provider_config = OAuthProviderConfig::new(
    "my-client-id",
    "my-client-secret",
    "https://example.com/auth/google/callback",
);
// Construct the provider (cookie_config, key come from your app config)
let http_client = reqwest::Client::new();
let google = Google::new(&provider_config, &cookie_config, &key, http_client);

// Login handler — returns a 303 redirect to Google's authorization page
async fn login_handler(
    // Google: impl OAuthProvider
) -> modo::Result<AuthorizationRequest> {
    google.authorize_url()
}

// Callback handler
async fn callback_handler(
    state: OAuthState,
    axum::extract::Query(params): axum::extract::Query<CallbackParams>,
) -> modo::Result<String> {
    let profile: UserProfile = google.exchange(&params, &state).await?;
    Ok(format!("logged in as {}", profile.email))
}
```

`UserProfile` fields common to all providers: `provider`, `provider_user_id`,
`email`, `email_verified`, `name`, `avatar_url`, and `raw` (full JSON from the
provider's user-info endpoint).

## Configuration

`PasswordConfig` and `TotpConfig` implement `serde::Deserialize` and `Default`,
so they can be embedded in a YAML config file:

```yaml
password:
    memory_cost_kib: 19456
    time_cost: 2
    parallelism: 1
    output_len: 32

totp:
    digits: 6
    step_secs: 30
    window: 1

jwt:
    secret: "${JWT_SECRET}"
    default_expiry: 3600
    leeway: 5
    issuer: "my-app"
    audience: "api"

oauth:
    google:
        client_id: "${GOOGLE_CLIENT_ID}"
        client_secret: "${GOOGLE_CLIENT_SECRET}"
        redirect_uri: "https://example.com/auth/google/callback"
    github:
        client_id: "${GITHUB_CLIENT_ID}"
        client_secret: "${GITHUB_CLIENT_SECRET}"
        redirect_uri: "https://example.com/auth/github/callback"
```

## Key Types

| Type                   | Path                   | Purpose                                             |
| ---------------------- | ---------------------- | --------------------------------------------------- |
| `PasswordConfig`       | `modo::auth::password` | Argon2id parameters                                 |
| `Totp`                 | `modo::auth::totp`     | TOTP authenticator instance                         |
| `TotpConfig`           | `modo::auth::totp`     | TOTP algorithm parameters                           |
| `JwtConfig`            | `modo::auth::jwt`      | JWT signing/validation config (also via crate root) |
| `JwtEncoder`           | `modo::auth::jwt`      | Signs and produces JWT strings                      |
| `JwtDecoder`           | `modo::auth::jwt`      | Verifies and parses JWT strings                     |
| `JwtLayer<T>`          | `modo::auth::jwt`      | Tower middleware for axum routes                    |
| `Claims<T>`            | `modo::auth::jwt`      | JWT claims struct; axum extractor                   |
| `Bearer`               | `modo::auth::jwt`      | Axum extractor for the raw Bearer token string      |
| `JwtError`             | `modo::auth::jwt`      | Typed JWT error enum with `code()` strings          |
| `HmacSigner`           | `modo::auth::jwt`      | HMAC-SHA256 (HS256) signer/verifier                 |
| `TokenSigner`          | `modo::auth::jwt`      | Trait for JWT signing (extends `TokenVerifier`)     |
| `TokenVerifier`        | `modo::auth::jwt`      | Trait for JWT signature verification                |
| `TokenSource`          | `modo::auth::jwt`      | Trait for pluggable token extraction                |
| `BearerSource`         | `modo::auth::jwt`      | Extracts token from `Authorization: Bearer` header  |
| `CookieSource`         | `modo::auth::jwt`      | Extracts token from a named cookie                  |
| `QuerySource`          | `modo::auth::jwt`      | Extracts token from a query parameter               |
| `HeaderSource`         | `modo::auth::jwt`      | Extracts token from a custom header                 |
| `Revocation`           | `modo::auth::jwt`      | Trait for async token revocation checks             |
| `ValidationConfig`     | `modo::auth::jwt`      | Runtime validation policy (leeway, iss, aud)        |
| `OAuthProvider`        | `modo::auth::oauth`    | Trait for custom OAuth 2.0 providers                |
| `Google`               | `modo::auth::oauth`    | Built-in Google OAuth 2.0 provider                  |
| `GitHub`               | `modo::auth::oauth`    | Built-in GitHub OAuth 2.0 provider                  |
| `OAuthConfig`          | `modo::auth::oauth`    | YAML config for all OAuth providers                 |
| `OAuthProviderConfig`  | `modo::auth::oauth`    | Per-provider credentials (client ID, secret, URI)   |
| `OAuthState`           | `modo::auth::oauth`    | Axum extractor for the signed OAuth state cookie    |
| `CallbackParams`       | `modo::auth::oauth`    | Query params delivered to the callback route        |
| `AuthorizationRequest` | `modo::auth::oauth`    | Response that redirects + sets the state cookie     |
| `UserProfile`          | `modo::auth::oauth`    | Normalized user data from any OAuth provider        |

JWT types and OAuth types are also re-exported at the `modo` crate root
(e.g., `modo::JwtEncoder`, `modo::Claims`, `modo::Google`) when the `auth`
feature is enabled.