12 KiB
SF Auth Middleware for Axum
Authentication middleware for Axum applications using the SnazzyFellas authentication service with tower_sessions for session management.
Features
- Struct Middleware: Tower-based middleware with dynamic redirect URI callback
- 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
- Flexible Redirects: Use a callback function to dynamically determine redirect URIs
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};
use sf_auth_middleware_axum::{SfAuthLayer, create_auth_callback, SfUser};
use tower_sessions::{MemoryStore, SessionManagerLayer};
#[tokio::main]
async fn main() {
// 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 - redirects to /dashboard after successful auth
.route("/auth/callback", get(create_auth_callback("/dashboard")))
// Protected routes
.route("/dashboard", get(dashboard))
// Apply authentication middleware - points to the callback route
.layer(SfAuthLayer::new(|_req| {
"http://localhost:3000/auth/callback".to_string()
}))
// 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 layer to routes that require authentication
- Session Check: The middleware checks for
sf_usernameandsf_user_idin the session - Redirect to SF Auth: If not authenticated, calls your callback to get the redirect URI (should point to your
/auth/callbackroute), then redirects to:https://snazzyfellas.com/api/redirect/authenticate?redirect_uri={your_callback_route} - SF Auth Validates: The SF server authenticates the user and redirects back to your callback route with credentials (
user_id,username,key) - Callback Validates: Your 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 - Final Redirect: Redirects to the URI specified when you created the callback (e.g.,
/dashboard) - Access Granted: Subsequent requests use the
SfUserextractor to access authenticated user data
Example Flow
User -> /dashboard (protected)
↓
Middleware checks session (not authenticated)
↓
Redirect to: https://snazzyfellas.com/api/redirect/authenticate?redirect_uri=http://myapp.com/auth/callback
↓
SF Auth validates user
↓
Redirect to: http://myapp.com/auth/callback?user_id=123&username=john&key=abc
↓
Callback validates with SF server
↓
Session set (sf_username, sf_user_id)
↓
Redirect to: /dashboard
↓
User accesses /dashboard (authenticated)
Architecture
Middleware Layer (SfAuthLayer)
The SfAuthLayer is a Tower middleware that checks authentication before allowing requests through. It takes a callback function that determines where users should be redirected after authentication.
Simple Static Redirect to Callback:
// Point to your callback route
let auth_layer = SfAuthLayer::new(|_req| {
"http://localhost:3000/auth/callback".to_string()
});
Dynamic Redirect Based on Environment:
let auth_layer = SfAuthLayer::new(|req| {
// Use different callback URLs for different environments
let host = req.headers()
.get("host")
.and_then(|h| h.to_str().ok())
.unwrap_or("localhost:3000");
format!("http://{}/auth/callback", host)
});
Multiple Callback Routes:
// Different sections can use different callback routes with different post-auth destinations
let admin_layer = SfAuthLayer::new(|_req| {
"http://myapp.com/admin/callback".to_string()
});
let user_layer = SfAuthLayer::new(|_req| {
"http://myapp.com/user/callback".to_string()
});
// Then define different callback handlers:
.route("/admin/callback", get(create_auth_callback("/admin/dashboard")))
.route("/user/callback", get(create_auth_callback("/user/profile")))
Callback Route (create_auth_callback)
Create a callback handler that specifies where to redirect users after successful authentication:
// Redirect to dashboard after auth
.route("/auth/callback", get(create_auth_callback("/dashboard")))
// Or use a full URL
.route("/auth/callback", get(create_auth_callback("https://myapp.com/dashboard")))
Important: The callback route must be publicly accessible (not behind the auth middleware).
The callback handler:
- Receives
user_id,username, andkeyas query parameters from SF auth server - Validates credentials with the SF server
- Sets session values (
sf_username,sf_user_id) on successful validation - Redirects to the specified URI on success
- Returns error on validation failure (fail-closed)
Middleware Configuration: The middleware's redirect URI callback should point to this callback route:
.layer(SfAuthLayer::new(|_req| {
"http://localhost:3000/auth/callback".to_string()
}))
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);
Advanced Usage
Protecting Specific Routes
You can apply the middleware to specific route groups:
let app = Router::new()
// Public routes
.route("/", get(home))
.route("/about", get(about))
// Public callback that redirects to admin dashboard
.route("/auth/callback", get(create_auth_callback("/admin/dashboard")))
// Protected admin routes
.nest("/admin", admin_routes().layer(SfAuthLayer::new(|_| {
"http://myapp.com/auth/callback".to_string()
})))
// Apply session layer to everything
.layer(session_layer);
fn admin_routes() -> Router {
Router::new()
.route("/dashboard", get(admin_dashboard))
.route("/users", get(admin_users))
}
Multiple Callback Routes for Different Sections
Different parts of your app can use different callback routes that redirect to different destinations:
let app = Router::new()
// Admin callback redirects to admin dashboard
.route("/admin/callback", get(create_auth_callback("/admin/dashboard")))
// User callback redirects to user profile
.route("/user/callback", get(create_auth_callback("/user/profile")))
// Admin section uses admin callback
.nest("/admin", admin_routes().layer(SfAuthLayer::new(|_| {
"http://myapp.com/admin/callback".to_string()
})))
// User section uses user callback
.nest("/user", user_routes().layer(SfAuthLayer::new(|_| {
"http://myapp.com/user/callback".to_string()
})))
.layer(session_layer);
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)http://localhost:3000/profile- Another protected page
API Reference
SfAuthLayer
pub struct SfAuthLayer { /* ... */ }
impl SfAuthLayer {
pub fn new<F>(redirect_uri_fn: F) -> Self
where
F: Fn(&Request) -> String + Send + Sync + 'static
}
SfUser
pub struct SfUser { /* ... */ }
impl SfUser {
pub fn username(&self) -> &str
pub fn user_id(&self) -> &str
}
create_auth_callback
pub fn create_auth_callback(
redirect_uri: impl Into<String>,
) -> impl Fn(Session, Query<CallbackQuery>) -> Future<Output = Result<Response, SfAuthError>>
Creates a handler that validates authentication and redirects to the specified URI on success.
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
- Callback Security: The
/auth/callbackroute validates all credentials before setting session
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_same_site(cookie::SameSite::Lax)
.with_expiry(Expiry::OnInactivity(Duration::hours(2)));
Troubleshooting
"No session found" errors
Make sure the SessionManagerLayer is applied AFTER your routes but BEFORE the SfAuthLayer:
let app = Router::new()
.route("/protected", get(handler))
.layer(SfAuthLayer::new(|_| "...".to_string())) // Auth layer first
.layer(session_layer); // Session layer last
Callback route requires authentication
The /auth/callback route must be defined BEFORE applying the auth layer, or it should be in a separate router that doesn't have the auth middleware.
Session not persisting
Ensure your session store is properly configured and that cookies are being set correctly (check HTTPS requirements for secure cookies).
License
This project is licensed under the MIT License.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.