Refactor middleware and make auth_callback redirect instead of success message
This commit is contained in:
parent
0d26e9326a
commit
3ec4360039
6 changed files with 383 additions and 185 deletions
237
README.md
237
README.md
|
|
@ -4,11 +4,12 @@ Authentication middleware for Axum applications using the SnazzyFellas authentic
|
|||
|
||||
## Features
|
||||
|
||||
- **Middleware**: Automatically redirect unauthenticated users to the SF auth endpoint
|
||||
- **Struct Middleware**: Tower-based middleware with dynamic redirect URI callback
|
||||
- **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
|
||||
- **Flexible Redirects**: Use a callback function to dynamically determine redirect URIs
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -25,27 +26,25 @@ tokio = { version = "1", features = ["full"] }
|
|||
## Quick Start
|
||||
|
||||
```rust
|
||||
use axum::{routing::get, Router, middleware};
|
||||
use sf_auth_middleware_axum::{SfAuthConfig, sf_auth_middleware, auth_callback, SfUser};
|
||||
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() {
|
||||
// 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))
|
||||
// Public callback route - redirects to /dashboard after successful auth
|
||||
.route("/auth/callback", get(create_auth_callback("/dashboard")))
|
||||
// Protected routes
|
||||
.route("/dashboard", get(dashboard))
|
||||
.layer(middleware::from_fn(move |session, req, next| {
|
||||
sf_auth_middleware(config.clone(), session, req, next)
|
||||
// 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);
|
||||
|
|
@ -62,58 +61,117 @@ async fn dashboard(user: SfUser) -> String {
|
|||
|
||||
## How It Works
|
||||
|
||||
1. **Protection**: Apply the middleware to routes that require authentication
|
||||
1. **Protection**: Apply the middleware layer 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:
|
||||
3. **Redirect to SF Auth**: If not authenticated, calls your callback to get the redirect URI (should point to your `/auth/callback` route), then redirects to:
|
||||
```
|
||||
https://snazzyfellas.com/api/redirect/authenticate?redirect_uri={your_configured_uri}
|
||||
https://snazzyfellas.com/api/redirect/authenticate?redirect_uri={your_callback_route}
|
||||
```
|
||||
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:
|
||||
4. **SF Auth Validates**: The SF server authenticates the user and redirects back to your callback route with credentials (`user_id`, `username`, `key`)
|
||||
5. **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": "..." }
|
||||
```
|
||||
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
|
||||
7. **Final Redirect**: Redirects to the URI specified when you created the callback (e.g., `/dashboard`)
|
||||
8. **Access Granted**: Subsequent requests use the `SfUser` extractor 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
|
||||
|
||||
### Configuration (`SfAuthConfig`)
|
||||
### Middleware Layer (`SfAuthLayer`)
|
||||
|
||||
Configure the redirect URI where users should land after authentication:
|
||||
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:**
|
||||
```rust
|
||||
let config = SfAuthConfig::new("https://myapp.com/dashboard");
|
||||
// Point to your callback route
|
||||
let auth_layer = SfAuthLayer::new(|_req| {
|
||||
"http://localhost:3000/auth/callback".to_string()
|
||||
});
|
||||
```
|
||||
|
||||
### Middleware (`sf_auth_middleware`)
|
||||
**Dynamic Redirect Based on Environment:**
|
||||
```rust
|
||||
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)
|
||||
});
|
||||
```
|
||||
|
||||
The middleware function checks authentication and redirects unauthenticated users:
|
||||
**Multiple Callback Routes:**
|
||||
```rust
|
||||
// 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:
|
||||
|
||||
```rust
|
||||
use axum::middleware;
|
||||
// Redirect to dashboard after auth
|
||||
.route("/auth/callback", get(create_auth_callback("/dashboard")))
|
||||
|
||||
.layer(middleware::from_fn(move |session, req, next| {
|
||||
sf_auth_middleware(config.clone(), session, req, next)
|
||||
// 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`, and `key` as 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:
|
||||
|
||||
```rust
|
||||
.layer(SfAuthLayer::new(|_req| {
|
||||
"http://localhost:3000/auth/callback".to_string()
|
||||
}))
|
||||
```
|
||||
|
||||
### Callback Route (`auth_callback`)
|
||||
|
||||
Mount this handler at `/auth/callback` to receive authentication callbacks:
|
||||
|
||||
```rust
|
||||
.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:
|
||||
|
|
@ -171,6 +229,54 @@ 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:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
|
@ -182,17 +288,19 @@ cargo run --example basic
|
|||
Then visit:
|
||||
- `http://localhost:3000/` - Public home page
|
||||
- `http://localhost:3000/dashboard` - Protected page (will redirect to SF auth)
|
||||
- `http://localhost:3000/profile` - Another protected page
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SfAuthConfig`
|
||||
### `SfAuthLayer`
|
||||
|
||||
```rust
|
||||
pub struct SfAuthConfig { /* ... */ }
|
||||
pub struct SfAuthLayer { /* ... */ }
|
||||
|
||||
impl SfAuthConfig {
|
||||
pub fn new(redirect_uri: impl Into<String>) -> Self
|
||||
pub fn redirect_uri(&self) -> &str
|
||||
impl SfAuthLayer {
|
||||
pub fn new<F>(redirect_uri_fn: F) -> Self
|
||||
where
|
||||
F: Fn(&Request) -> String + Send + Sync + 'static
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -207,25 +315,15 @@ impl SfUser {
|
|||
}
|
||||
```
|
||||
|
||||
### `sf_auth_middleware`
|
||||
### `create_auth_callback`
|
||||
|
||||
```rust
|
||||
pub async fn sf_auth_middleware(
|
||||
config: SfAuthConfig,
|
||||
session: Session,
|
||||
req: Request,
|
||||
next: Next,
|
||||
) -> Response
|
||||
pub fn create_auth_callback(
|
||||
redirect_uri: impl Into<String>,
|
||||
) -> impl Fn(Session, Query<CallbackQuery>) -> Future<Output = Result<Response, SfAuthError>>
|
||||
```
|
||||
|
||||
### `auth_callback`
|
||||
|
||||
```rust
|
||||
pub async fn auth_callback(
|
||||
session: Session,
|
||||
Query(params): Query<CallbackQuery>,
|
||||
) -> Result<Response, SfAuthError>
|
||||
```
|
||||
Creates a handler that validates authentication and redirects to the specified URI on success.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
|
|
@ -233,6 +331,7 @@ pub async fn auth_callback(
|
|||
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
|
||||
5. **Callback Security**: The `/auth/callback` route validates all credentials before setting session
|
||||
|
||||
Example secure session configuration:
|
||||
|
||||
|
|
@ -243,9 +342,31 @@ 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`:
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue