auth-framework 0.5.0-rc18

A comprehensive, production-ready authentication and authorization framework for Rust applications
Documentation
# Integration Patterns and Best Practices


This guide focuses on patterns that match the current crate surface.

AuthFramework does not currently expose a general-purpose Rust `AuthClient` or `ServiceAuthClient` type for the REST API. The recommended integration model is:

1. Run `ApiServer` inside your auth service.
2. Build a small application-specific HTTP adapter around the routes you use.
3. Reuse the shared `ApiResponse<T>` envelope in that adapter.

## 1. Thin HTTP Adapter


Use a normal HTTP client such as `reqwest::Client` and model only the routes your application actually calls.

```rust,ignore
use reqwest::Client;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]

struct ApiResponse<T> {
    success: bool,
    data: Option<T>,
    error: Option<ApiError>,
    message: Option<String>,
}

#[derive(Debug, Deserialize)]

struct ApiError {
    code: String,
    message: String,
    details: Option<serde_json::Value>,
}

#[derive(Debug, Serialize)]

struct LoginRequest<'a> {
    username: &'a str,
    password: &'a str,
    remember_me: bool,
}

#[derive(Debug, Deserialize)]

struct LoginUserInfo {
    id: String,
    username: String,
    roles: Vec<String>,
    permissions: Vec<String>,
}

#[derive(Debug, Deserialize)]

struct LoginResponse {
    access_token: String,
    refresh_token: String,
    token_type: String,
    expires_in: u64,
    user: LoginUserInfo,
    login_risk_level: String,
    security_warnings: Vec<String>,
}

struct AuthApiClient {
    base_url: String,
    http: Client,
}

impl AuthApiClient {
    async fn login(&self, username: &str, password: &str) -> anyhow::Result<LoginResponse> {
        let response = self
            .http
            .post(format!("{}/api/v1/auth/login", self.base_url))
            .json(&LoginRequest {
                username,
                password,
                remember_me: false,
            })
            .send()
            .await?
            .error_for_status()?;

        let body: ApiResponse<LoginResponse> = response.json().await?;
        match (body.success, body.data, body.error) {
            (true, Some(data), _) => Ok(data),
            (false, _, Some(err)) => Err(anyhow::anyhow!("{}: {}", err.code, err.message)),
            _ => Err(anyhow::anyhow!("unexpected response shape")),
        }
    }
}
```

## 2. Axum Middleware Pattern


For services that need to trust AuthFramework bearer tokens, call `GET /api/v1/auth/validate` once near the edge and stash the resulting identity in request extensions.

```rust,ignore
use axum::{
    extract::State,
    http::{Request, StatusCode},
    middleware::Next,
    response::Response,
};

#[derive(Clone)]

struct AppState {
    auth_api: AuthApiClient,
}

async fn auth_middleware<B>(
    State(state): State<AppState>,
    mut request: Request<B>,
    next: Next<B>,
) -> Result<Response, StatusCode> {
    let bearer = request
        .headers()
        .get("authorization")
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.strip_prefix("Bearer "))
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let user = state
        .auth_api
        .validate_token(bearer)
        .await
        .map_err(|_| StatusCode::UNAUTHORIZED)?;

    request.extensions_mut().insert(user);
    Ok(next.run(request).await)
}
```

Why `GET /auth/validate`:

- It matches the mounted route.
- It returns the normalized user identity used by the REST layer.
- It also respects token revocation checks already wired into the API.

## 3. MFA Enrollment Pattern


The current MFA enrollment flow is two-step and stays entirely on the authenticated API:

1. `POST /api/v1/mfa/setup`
2. Render the returned `qr_code` or `secret`
3. `POST /api/v1/mfa/verify` with `totp_code`

Do not build a separate `verify-setup` route on the client side. That endpoint does not exist in the current server.

If login returns `MFA_REQUIRED`, finish the login by calling `POST /api/v1/auth/login` again with `challenge_id` and `mfa_code`.

## 4. Profile and Session Management Pattern


Use `/users/profile` for editable account data and `/users/me` only when you specifically want OIDC-style claims.

Current route split:

- `/api/v1/users/profile` returns `email_verified`, `mfa_enabled`, and the editable profile fields.
- `/api/v1/users/me` aliases `/api/v1/oauth/userinfo` and returns OIDC claims instead.
- `/api/v1/users/sessions` lists active sessions for the bearer token's user.

## 5. Admin Automation Pattern


Treat the admin routes as operational APIs rather than end-user workflows.

- Use `GET /api/v1/admin/users` for paginated user inventory.
- Use `PUT /api/v1/admin/users/{user_id}/roles` only for role replacement.
- Use `PUT /api/v1/admin/users/{user_id}/activate` with `{ "active": true | false }` for account state changes.
- Use `GET /api/v1/admin/config` and `PUT /api/v1/admin/config` for runtime-tunable settings only.

## 6. OAuth and OIDC Pattern


Prefer standards-compliant clients around the RFC endpoints instead of inventing crate-local wrapper types.

Important route details:

- Authorization endpoint: `GET /api/v1/oauth/authorize`
- Token endpoint: `POST /api/v1/oauth/token`
- Introspection endpoint: `POST /api/v1/oauth/introspect`
- Discovery document: `GET /api/v1/.well-known/openid-configuration`
- JWKS: `GET /api/v1/.well-known/jwks.json`

Current caveats:

- `/oauth/introspect` requires client authentication and rejects bearer-token auth.
- Dynamic client registration is `POST /oauth/register`.
- Client lookup is `GET /oauth/clients/{client_id}`.

## 7. Testing Pattern


For Rust integration tests, test the mounted HTTP routes directly instead of depending on non-existent mock client types.

```rust,ignore
#[tokio::test]

async fn login_returns_wrapped_tokens() {
    let auth = std::sync::Arc::new(setup_auth_framework().await);
    let server = auth_framework::api::ApiServer::new(auth);
    let router = server.build_router().await.unwrap();

    let response = tower::ServiceExt::oneshot(
        router,
        axum::http::Request::builder()
            .method("POST")
            .uri("/api/v1/auth/login")
            .header("content-type", "application/json")
            .body(axum::body::Body::from(
                r#"{"username":"alice","password":"SecurePassword123!"}"#,
            ))
            .unwrap(),
    )
    .await
    .unwrap();

    assert_eq!(response.status(), axum::http::StatusCode::OK);
}
```

## 8. Operational Guidance


- Reuse one `reqwest::Client` per process to benefit from connection pooling.
- Unwrap the API envelope once in your adapter, not in every call site.
- Preserve `error.code` in logs and metrics. It is the stable signal for operational analysis.
- Keep protocol-native RFC endpoints separate from your wrapped application routes in client code.