fix(auth): support session cookie authentication in API endpoints
- Update AuthenticatedUser extractor to check session cookies first
- Add authenticate_via_session() helper function
- Session cookies now work for all API write operations
- Change SameSite to Strict for better CSRF protection
- Add BREWLOG_SECURE_COOKIES env var to enable secure flag in production
This fixes the bug where authenticated frontend users got 401 errors
when submitting forms. API endpoints now accept both Bearer tokens
and session cookies for authentication.
All 65 tests pass (8 unit + 42 server + 15 CLI) ✅
Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com>
This commit is contained in:
parent
4c040f2c58
commit
b46295d0cf
2 changed files with 39 additions and 3 deletions
|
|
@ -5,11 +5,14 @@ use axum::{
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
use tower_cookies::Cookies;
|
||||||
|
|
||||||
use crate::domain::users::User;
|
use crate::domain::users::User;
|
||||||
use crate::infrastructure::auth::hash_token;
|
use crate::infrastructure::auth::hash_token;
|
||||||
use crate::server::server::AppState;
|
use crate::server::server::AppState;
|
||||||
|
|
||||||
|
const SESSION_COOKIE_NAME: &str = "brewlog_session";
|
||||||
|
|
||||||
/// Extension type to carry authenticated user through request handlers
|
/// Extension type to carry authenticated user through request handlers
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AuthenticatedUser(pub User);
|
pub struct AuthenticatedUser(pub User);
|
||||||
|
|
@ -27,7 +30,14 @@ impl FromRequestParts<AppState> for AuthenticatedUser {
|
||||||
return Ok(user.clone());
|
return Ok(user.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, extract from Authorization header directly
|
// Try to authenticate via session cookie first
|
||||||
|
if let Ok(cookies) = Cookies::from_request_parts(parts, state).await {
|
||||||
|
if let Some(user) = authenticate_via_session(state, &cookies).await {
|
||||||
|
return Ok(AuthenticatedUser(user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to Bearer token authentication
|
||||||
let auth_header = parts
|
let auth_header = parts
|
||||||
.headers
|
.headers
|
||||||
.get(header::AUTHORIZATION)
|
.get(header::AUTHORIZATION)
|
||||||
|
|
@ -73,6 +83,27 @@ impl FromRequestParts<AppState> for AuthenticatedUser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Authenticate via session cookie
|
||||||
|
async fn authenticate_via_session(state: &AppState, cookies: &Cookies) -> Option<User> {
|
||||||
|
let cookie = cookies.get(SESSION_COOKIE_NAME)?;
|
||||||
|
let session_token = cookie.value();
|
||||||
|
let session_token_hash = hash_token(session_token);
|
||||||
|
|
||||||
|
// Check if session exists and is valid
|
||||||
|
let session = state
|
||||||
|
.session_repo
|
||||||
|
.get_by_token_hash(&session_token_hash)
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
if session.is_expired() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user
|
||||||
|
state.user_repo.get(session.user_id).await.ok()
|
||||||
|
}
|
||||||
|
|
||||||
/// Middleware that optionally extracts authentication from the request
|
/// Middleware that optionally extracts authentication from the request
|
||||||
/// Sets AuthenticatedUser extension if valid token is found
|
/// Sets AuthenticatedUser extension if valid token is found
|
||||||
pub async fn optional_auth_middleware(
|
pub async fn optional_auth_middleware(
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,13 @@ pub(crate) async fn login_submit(
|
||||||
let mut cookie = Cookie::new(SESSION_COOKIE_NAME, session_token);
|
let mut cookie = Cookie::new(SESSION_COOKIE_NAME, session_token);
|
||||||
cookie.set_path("/");
|
cookie.set_path("/");
|
||||||
cookie.set_http_only(true);
|
cookie.set_http_only(true);
|
||||||
cookie.set_same_site(tower_cookies::cookie::SameSite::Lax);
|
cookie.set_same_site(tower_cookies::cookie::SameSite::Strict);
|
||||||
// In production, set secure flag: cookie.set_secure(true);
|
|
||||||
|
// Enable secure flag if BREWLOG_SECURE_COOKIES is set to "true"
|
||||||
|
// This should be enabled in production when serving over HTTPS
|
||||||
|
if std::env::var("BREWLOG_SECURE_COOKIES").unwrap_or_default() == "true" {
|
||||||
|
cookie.set_secure(true);
|
||||||
|
}
|
||||||
|
|
||||||
cookies.add(cookie);
|
cookies.add(cookie);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue