7 KiB
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
SfUserextractor - 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
- Protection: Apply the middleware to routes that require authentication
- Session Check: The middleware checks for
sf_usernameandsf_user_idin the session - Redirect: If not authenticated, redirects to the SF authentication endpoint:
https://snazzyfellas.com/api/redirect/authenticate?redirect_uri={your_configured_uri} - Callback: The SF server redirects back to
/auth/callbackwith credentials (user_id,username,key) - 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": "..." } - Session Setup: On successful validation, sets
sf_usernameandsf_user_idin the session - Access Granted: Use the
SfUserextractor 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, andkeyas 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 usernameuser.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 usernamesf_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 pagehttp://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
- HTTPS Required: Always use HTTPS in production for session security
- Secure Sessions: Configure session cookies with
secureandhttponlyflags - Session Expiry: Set appropriate session expiration times
- 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.