No description
Find a file
2026-02-01 17:37:00 -08:00
examples Move lib to root dir 2026-02-01 17:37:00 -08:00
src Move lib to root dir 2026-02-01 17:37:00 -08:00
.envrc Initial commit 2026-02-01 16:13:42 -08:00
.gitignore Move lib to root dir 2026-02-01 17:37:00 -08:00
Cargo.lock Move lib to root dir 2026-02-01 17:37:00 -08:00
Cargo.toml Move lib to root dir 2026-02-01 17:37:00 -08:00
devenv.lock Initial commit 2026-02-01 16:13:42 -08:00
devenv.nix Initial commit 2026-02-01 16:13:42 -08:00
devenv.yaml Initial commit 2026-02-01 16:13:42 -08:00
README.md Move lib to root dir 2026-02-01 17:37:00 -08:00

SF Auth Middleware for Axum

Authentication middleware for Axum applications using the SnazzyFellas authentication service with tower_sessions for session management.

Features

  • Middleware: Automatically redirect unauthenticated users to the SF auth endpoint
  • Extractor: Type-safe access to authenticated user information via the SfUser extractor
  • Callback Handler: Ready-to-use route handler for authentication callbacks
  • Session Integration: Seamless integration with tower-sessions
  • Fail-Closed Security: Validation failures result in denied access, not automatic approval

Installation

Add this to your Cargo.toml:

[dependencies]
sf-auth-middleware-axum = "0.1"
axum = "0.8"
tower-sessions = "0.15"
tokio = { version = "1", features = ["full"] }

Quick Start

use axum::{routing::get, Router, middleware};
use sf_auth_middleware_axum::{SfAuthConfig, sf_auth_middleware, auth_callback, SfUser};
use tower_sessions::{MemoryStore, SessionManagerLayer};

#[tokio::main]
async fn main() {
    // Configure the authentication middleware
    let config = SfAuthConfig::new("https://myapp.com/dashboard");
    
    // Set up session store
    let session_store = MemoryStore::default();
    let session_layer = SessionManagerLayer::new(session_store);

    // Build your application
    let app = Router::new()
        // Public callback route (no auth required)
        .route("/auth/callback", get(auth_callback))
        // Protected routes
        .route("/dashboard", get(dashboard))
        .layer(middleware::from_fn(move |session, req, next| {
            sf_auth_middleware(config.clone(), session, req, next)
        }))
        // Add session layer
        .layer(session_layer);

    // Run the server
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn dashboard(user: SfUser) -> String {
    format!("Hello, {}! Your ID: {}", user.username(), user.user_id())
}

How It Works

  1. Protection: Apply the middleware to routes that require authentication
  2. Session Check: The middleware checks for sf_username and sf_user_id in the session
  3. Redirect: If not authenticated, redirects to the SF authentication endpoint:
    https://snazzyfellas.com/api/redirect/authenticate?redirect_uri={your_configured_uri}
    
  4. Callback: The SF server redirects back to /auth/callback with credentials (user_id, username, key)
  5. Validation: The callback handler validates credentials with the SF server:
    POST https://snazzyfellas.com/api/redirect/validate
    Body: { "user_id": "...", "key": "..." }
    Response: { "valid": true, "user_id": "..." }
    
  6. Session Setup: On successful validation, sets sf_username and sf_user_id in the session
  7. Access Granted: Use the SfUser extractor in handlers to access authenticated user data

Architecture

Configuration (SfAuthConfig)

Configure the redirect URI where users should land after authentication:

let config = SfAuthConfig::new("https://myapp.com/dashboard");

Middleware (sf_auth_middleware)

The middleware function checks authentication and redirects unauthenticated users:

use axum::middleware;

.layer(middleware::from_fn(move |session, req, next| {
    sf_auth_middleware(config.clone(), session, req, next)
}))

Callback Route (auth_callback)

Mount this handler at /auth/callback to receive authentication callbacks:

.route("/auth/callback", get(auth_callback))

This route:

  • Receives user_id, username, and key as query parameters
  • Validates credentials with the SF server
  • Sets session values on successful validation
  • Returns error on validation failure (fail-closed)

Extractor (SfUser)

Use the SfUser extractor in your handlers to access authenticated user data:

async fn protected_handler(user: SfUser) -> String {
    format!("Username: {}, ID: {}", user.username(), user.user_id())
}

The extractor provides:

  • user.username() - The authenticated user's username
  • user.user_id() - The authenticated user's ID

If the session doesn't contain valid credentials, the extractor returns a 401 Unauthorized error.

Session Keys

The middleware uses fixed session keys for consistency:

  • sf_username - Stores the authenticated user's username
  • sf_user_id - Stores the authenticated user's ID

Error Handling

The library uses a fail-closed security model:

  • Network Errors: If validation API calls fail, authentication is denied
  • Invalid Response: Malformed responses from the validation endpoint result in denied access
  • Validation Failure: If the SF server returns valid: false, session is not set
  • User ID Mismatch: If the returned user_id doesn't match the request, authentication is denied

All errors implement Axum's IntoResponse trait for automatic HTTP error responses.

Session Store

This library works with any tower-sessions store. Common options:

Memory Store (Development)

use tower_sessions::MemoryStore;
let session_store = MemoryStore::default();

Redis Store (Production)

use tower_sessions_redis_store::RedisStore;
let pool = deadpool_redis::Pool::new(...);
let session_store = RedisStore::new(pool);

PostgreSQL Store (Production)

use tower_sessions_sqlx_store::PostgresStore;
let pool = sqlx::PgPool::connect("...").await?;
let session_store = PostgresStore::new(pool);

Examples

Run the included example:

cargo run --example basic

Then visit:

  • http://localhost:3000/ - Public home page
  • http://localhost:3000/dashboard - Protected page (will redirect to SF auth)

API Reference

SfAuthConfig

pub struct SfAuthConfig { /* ... */ }

impl SfAuthConfig {
    pub fn new(redirect_uri: impl Into<String>) -> Self
    pub fn redirect_uri(&self) -> &str
}

SfUser

pub struct SfUser { /* ... */ }

impl SfUser {
    pub fn username(&self) -> &str
    pub fn user_id(&self) -> &str
}

sf_auth_middleware

pub async fn sf_auth_middleware(
    config: SfAuthConfig,
    session: Session,
    req: Request,
    next: Next,
) -> Response

auth_callback

pub async fn auth_callback(
    session: Session,
    Query(params): Query<CallbackQuery>,
) -> Result<Response, SfAuthError>

Security Considerations

  1. HTTPS Required: Always use HTTPS in production for session security
  2. Secure Sessions: Configure session cookies with secure and httponly flags
  3. Session Expiry: Set appropriate session expiration times
  4. Fail-Closed: The middleware denies access on any validation errors

Example secure session configuration:

use tower_sessions::Expiry;
use time::Duration;

let session_layer = SessionManagerLayer::new(session_store)
    .with_secure(true)
    .with_http_only(true)
    .with_expiry(Expiry::OnInactivity(Duration::hours(2)));

License

This project is licensed under the MIT License.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.