Files
Oxicloud/src/interfaces/api/handlers/auth_handler.rs
T

632 lines
21 KiB
Rust
Raw Normal View History

2025-03-20 09:22:31 +01:00
use axum::{
Router,
2026-02-14 01:29:34 +01:00
extract::{Json, Query, State},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Redirect, Response},
2026-02-14 01:29:34 +01:00
routing::{get, post, put},
2025-03-20 09:22:31 +01:00
};
2026-02-14 01:29:34 +01:00
use std::sync::Arc;
2025-03-20 09:22:31 +01:00
use crate::application::dtos::user_dto::{
2026-02-14 01:29:34 +01:00
ChangePasswordDto, LoginDto, OidcCallbackQueryDto, OidcExchangeDto, OidcProviderInfoDto,
RefreshTokenDto, RegisterDto, SetupAdminDto,
2025-03-20 09:22:31 +01:00
};
2026-02-14 01:29:34 +01:00
use crate::common::di::AppState;
use crate::interfaces::api::cookie_auth;
use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::auth::CurrentUserId;
2025-03-20 09:22:31 +01:00
pub fn auth_routes() -> Router<Arc<AppState>> {
// Routes that do NOT require authentication
2026-02-03 17:59:04 +01:00
let public_routes = Router::new()
.route("/status", get(get_system_status))
// OIDC endpoints (all public)
.route("/oidc/providers", get(oidc_providers))
.route("/oidc/authorize", get(oidc_authorize))
.route("/oidc/callback", get(oidc_callback))
.route("/oidc/exchange", post(oidc_exchange));
2026-02-14 01:29:34 +01:00
// Routes that DO require authentication - we use route_layer to apply middleware
// The middleware will use the state passed with .with_state() from main.rs
2026-02-03 17:59:04 +01:00
let protected_routes = Router::new()
2025-03-20 09:22:31 +01:00
.route("/me", get(get_current_user))
.route("/change-password", put(change_password))
2026-02-03 17:59:04 +01:00
.route("/logout", post(logout));
2026-02-14 01:29:34 +01:00
// Combine public and protected routes
2026-02-03 17:59:04 +01:00
public_routes.merge(protected_routes)
2025-03-20 09:22:31 +01:00
}
/// Rate-limited auth routes — split out so main.rs can apply per-endpoint
/// rate limiting middleware independently.
pub fn login_route() -> Router<Arc<AppState>> {
Router::new().route("/login", post(login))
}
pub fn register_route() -> Router<Arc<AppState>> {
Router::new().route("/register", post(register))
}
pub fn refresh_route() -> Router<Arc<AppState>> {
Router::new().route("/refresh", post(refresh_token))
}
/// Public setup route — only active before the first admin is created.
pub fn setup_route() -> Router<Arc<AppState>> {
Router::new().route("/setup", post(setup_admin))
}
2025-03-20 09:22:31 +01:00
async fn register(
State(state): State<Arc<AppState>>,
Json(dto): Json<RegisterDto>,
) -> Result<impl IntoResponse, AppError> {
2025-03-23 22:44:18 +01:00
// Add detailed logging for debugging
tracing::info!("Registration attempt for user: {}", dto.username);
2026-02-14 01:29:34 +01:00
2025-03-23 22:44:18 +01:00
// Verify auth service exists
let auth_service = match state.auth_service.as_ref() {
Some(service) => {
tracing::info!("Auth service found, proceeding with registration");
service
2026-02-14 01:29:34 +01:00
}
2025-03-23 22:44:18 +01:00
None => {
tracing::error!("Auth service not configured");
2026-02-14 01:29:34 +01:00
return Err(AppError::internal_error(
"Authentication service not configured",
));
2025-03-23 22:44:18 +01:00
}
};
// Fix #5: Block password registration when OIDC-only mode is active
2026-02-14 01:29:34 +01:00
if auth_service
.auth_application_service
.password_login_disabled()
{
return Err(AppError::new(
StatusCode::FORBIDDEN,
"Password registration is disabled. Please use SSO/OIDC to sign in.",
"PasswordRegistrationDisabled",
));
}
// Check if public registration has been disabled by the admin
if let Some(admin_svc) = state.admin_settings_service.as_ref()
2026-02-14 01:29:34 +01:00
&& !admin_svc.get_registration_enabled().await
{
return Err(AppError::new(
StatusCode::FORBIDDEN,
"Public registration has been disabled by the administrator.",
"RegistrationDisabled",
));
}
// Registration logic (admin detection, fresh-install handling, duplicate
// checks) is all inside the service layer. Call it directly.
2026-02-14 01:29:34 +01:00
match auth_service
.auth_application_service
.register(dto.clone())
.await
{
2025-03-23 22:44:18 +01:00
Ok(user) => {
tracing::info!("Registration successful for user: {}", dto.username);
Ok((StatusCode::CREATED, Json(user)))
2026-02-14 01:29:34 +01:00
}
2025-03-23 22:44:18 +01:00
Err(err) => {
tracing::error!("Registration failed for user {}: {}", dto.username, err);
Err(err.into())
}
}
2025-03-20 09:22:31 +01:00
}
async fn login(
State(state): State<Arc<AppState>>,
Json(dto): Json<LoginDto>,
) -> Result<Response, AppError> {
2025-03-23 22:44:18 +01:00
// Add detailed logging for debugging
tracing::info!("Login attempt for user: {}", dto.username);
2026-02-14 01:29:34 +01:00
// Verify auth service exists
2025-03-23 22:44:18 +01:00
let auth_service = match state.auth_service.as_ref() {
Some(service) => {
tracing::info!("Auth service found, proceeding with login");
service
2026-02-14 01:29:34 +01:00
}
2025-03-23 22:44:18 +01:00
None => {
tracing::error!("Auth service not configured");
2026-02-14 01:29:34 +01:00
return Err(AppError::internal_error(
"Authentication service not configured",
));
2025-03-23 22:44:18 +01:00
}
};
// ── Account lockout check ──────────────────────────────────────────
// Reject immediately if the account has too many consecutive failures.
// This runs BEFORE Argon2 to save CPU under brute-force attacks.
if let Err(lockout_secs) = auth_service.login_lockout.check(&dto.username) {
tracing::warn!(
username = %dto.username,
lockout_secs = lockout_secs,
"Login rejected — account temporarily locked"
);
return Err(AppError::new(
StatusCode::TOO_MANY_REQUESTS,
format!(
"Account temporarily locked due to too many failed attempts. Try again in {} seconds.",
lockout_secs
),
"AccountLocked",
));
}
// Check if password login is disabled (OIDC-only mode)
2026-02-14 01:29:34 +01:00
if auth_service
.auth_application_service
.password_login_disabled()
{
return Err(AppError::unauthorized(
2026-02-14 01:29:34 +01:00
"Password login is disabled. Please use SSO/OIDC to sign in.",
));
}
2026-02-14 01:29:34 +01:00
2025-03-23 22:44:18 +01:00
// Try the normal login process
2026-02-14 01:29:34 +01:00
match auth_service
.auth_application_service
.login(dto.clone())
.await
{
2025-03-23 22:44:18 +01:00
Ok(auth_response) => {
// ── Successful login — reset lockout counter ──
auth_service.login_lockout.record_success(&dto.username);
2025-03-23 22:44:18 +01:00
tracing::info!("Login successful for user: {}", dto.username);
// Log the response structure for debugging
tracing::debug!("Auth response: {:?}", &auth_response);
2026-02-14 01:29:34 +01:00
// Ensure the response has the expected fields
if auth_response.access_token.is_empty() || auth_response.refresh_token.is_empty() {
2026-02-14 01:29:34 +01:00
tracing::error!(
"Login response contains empty tokens for user: {}",
dto.username
);
return Err(AppError::internal_error(
"Error generating authentication tokens",
));
}
2026-02-14 01:29:34 +01:00
// ── Set HttpOnly cookies so the browser never stores tokens in JS ──
let mut response = (StatusCode::OK, Json(&auth_response)).into_response();
cookie_auth::append_auth_cookies(
response.headers_mut(),
&auth_response.access_token,
&auth_response.refresh_token,
auth_response.expires_in,
state.core.config.auth.refresh_token_expiry_secs,
);
2026-03-03 01:49:18 +01:00
cookie_auth::append_csrf_cookie(response.headers_mut(), auth_response.expires_in);
Ok(response)
2026-02-14 01:29:34 +01:00
}
2025-03-23 22:44:18 +01:00
Err(err) => {
// ── Record failed attempt for lockout tracking ──
auth_service.login_lockout.record_failure(&dto.username);
2025-03-23 22:44:18 +01:00
tracing::error!("Login failed for user {}: {}", dto.username, err);
Err(err.into())
}
}
2025-03-20 09:22:31 +01:00
}
/// Token refresh — accepts the refresh token from **either**:
/// 1. JSON body `{ "refresh_token": "..." }` (API clients, backward compat)
/// 2. HttpOnly cookie `oxicloud_refresh` (browsers)
2025-03-20 09:22:31 +01:00
async fn refresh_token(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
body: axum::body::Bytes,
) -> Result<Response, AppError> {
tracing::info!("Token refresh requested");
2026-02-14 01:29:34 +01:00
let auth_service = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
2026-02-14 01:29:34 +01:00
// Try JSON body first (backward compat), then fall back to HttpOnly cookie
let refresh_tok = serde_json::from_slice::<RefreshTokenDto>(&body)
.ok()
.map(|dto| dto.refresh_token)
.or_else(|| cookie_auth::extract_cookie_value(&headers, cookie_auth::REFRESH_COOKIE))
.ok_or_else(|| AppError::unauthorized("Refresh token required (JSON body or cookie)"))?;
let dto = RefreshTokenDto {
refresh_token: refresh_tok,
};
2026-02-14 01:29:34 +01:00
let auth_response = auth_service
.auth_application_service
.refresh_token(dto)
.await?;
2025-03-31 06:20:15 +02:00
tracing::info!("Token refresh successful, new token issued");
2026-02-14 01:29:34 +01:00
let mut response = (StatusCode::OK, Json(&auth_response)).into_response();
cookie_auth::append_auth_cookies(
response.headers_mut(),
&auth_response.access_token,
&auth_response.refresh_token,
auth_response.expires_in,
state.core.config.auth.refresh_token_expiry_secs,
);
2026-03-03 01:49:18 +01:00
cookie_auth::append_csrf_cookie(response.headers_mut(), auth_response.expires_in);
Ok(response)
2025-03-20 09:22:31 +01:00
}
async fn get_current_user(
State(state): State<Arc<AppState>>,
CurrentUserId(user_id): CurrentUserId,
2025-03-20 09:22:31 +01:00
) -> Result<impl IntoResponse, AppError> {
2026-02-14 01:29:34 +01:00
let auth_service = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
2026-02-14 01:29:34 +01:00
// First, update the storage usage statistics
// IMPORTANT: We await the calculation to return updated data
2025-04-09 00:21:20 +02:00
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() {
// Calculate storage synchronously (we await the result)
2026-02-14 01:29:34 +01:00
match storage_usage_service
.update_user_storage_usage(&user_id)
.await
{
2026-02-03 17:59:04 +01:00
Ok(usage) => {
2026-02-14 01:29:34 +01:00
tracing::info!(
"Updated storage usage for user {}: {} bytes",
user_id,
usage
);
}
2026-02-03 17:59:04 +01:00
Err(e) => {
// Only log a warning, don't fail the entire request
2026-02-03 17:59:04 +01:00
tracing::warn!("Failed to update storage usage for user {}: {}", user_id, e);
2025-04-09 00:21:20 +02:00
}
2026-02-03 17:59:04 +01:00
}
2025-04-09 00:21:20 +02:00
}
2026-02-14 01:29:34 +01:00
// Now get the user data WITH the updated storage
2026-02-14 01:29:34 +01:00
let user = auth_service
.auth_application_service
.get_user_by_id(&user_id)
.await?;
2025-03-20 09:22:31 +01:00
Ok((StatusCode::OK, Json(user)))
}
async fn change_password(
State(state): State<Arc<AppState>>,
CurrentUserId(user_id): CurrentUserId,
2025-03-20 09:22:31 +01:00
Json(dto): Json<ChangePasswordDto>,
) -> Result<impl IntoResponse, AppError> {
2026-02-14 01:29:34 +01:00
let auth_service = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
2026-02-14 01:29:34 +01:00
auth_service
.auth_application_service
.change_password(&user_id, dto)
2026-02-14 01:29:34 +01:00
.await?;
2025-03-20 09:22:31 +01:00
Ok(StatusCode::OK)
}
async fn logout(
State(state): State<Arc<AppState>>,
CurrentUserId(user_id): CurrentUserId,
headers: HeaderMap,
body: axum::body::Bytes,
) -> Result<Response, AppError> {
2026-02-14 01:29:34 +01:00
let auth_service = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
2026-02-14 01:29:34 +01:00
// Extract the REFRESH token (not the access token) so the service can
// look up and revoke the correct session.
// Strategy: try JSON body first (API clients), then HttpOnly cookie (browsers).
let refresh_token = serde_json::from_slice::<RefreshTokenDto>(&body)
.ok()
.map(|dto| dto.refresh_token)
.or_else(|| cookie_auth::extract_cookie_value(&headers, cookie_auth::REFRESH_COOKIE))
.ok_or_else(|| AppError::unauthorized("Refresh token required for logout (JSON body or cookie)"))?;
2026-02-14 01:29:34 +01:00
auth_service
.auth_application_service
.logout(&user_id, &refresh_token)
2026-02-14 01:29:34 +01:00
.await?;
// Clear HttpOnly + CSRF cookies so the browser forgets the session
let mut response = StatusCode::OK.into_response();
cookie_auth::append_clear_cookies(response.headers_mut());
cookie_auth::append_clear_csrf_cookie(response.headers_mut());
Ok(response)
2025-03-20 09:22:31 +01:00
}
/// POST /api/setup — One-time endpoint to create the first admin user.
///
/// Requires the setup token that was printed to the server log on first boot.
/// Once the admin is created, the system is marked as initialized and this
/// endpoint returns 403 for all subsequent requests.
async fn setup_admin(
State(state): State<Arc<AppState>>,
Json(dto): Json<SetupAdminDto>,
) -> Result<impl IntoResponse, AppError> {
tracing::info!("Setup admin request received for user: {}", dto.username);
// 1. Verify auth service exists
let auth_service = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
// 2. Check if system is already initialized (fail-closed: DB error → deny)
let admin_svc = state
.admin_settings_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Admin settings service not configured"))?;
if admin_svc.is_system_initialized().await {
tracing::warn!(
"Setup admin rejected: system already initialized (user: {})",
dto.username
);
return Err(AppError::new(
StatusCode::FORBIDDEN,
"System is already initialized. Use the admin panel to manage users.",
"SystemAlreadyInitialized",
));
}
// 3. Verify the one-time setup token
let expected_token = state.setup_token.as_deref().ok_or_else(|| {
AppError::new(
StatusCode::FORBIDDEN,
"No setup token available. The system may already be initialized or the server needs to be restarted.",
"NoSetupToken",
)
})?;
if !constant_time_eq(dto.setup_token.as_bytes(), expected_token.as_bytes()) {
tracing::warn!(
"Setup admin rejected: invalid setup token (user: {})",
dto.username
);
return Err(AppError::new(
StatusCode::FORBIDDEN,
"Invalid setup token. Check the server log for the correct token.",
"InvalidSetupToken",
));
}
// 4. Create the first admin user
let user = auth_service
.auth_application_service
.setup_create_admin(dto.username.clone(), dto.email, dto.password)
.await
.map_err(|e| {
tracing::error!("Setup admin creation failed: {}", e);
AppError::from(e)
})?;
// 5. Mark system as initialized
if let Err(e) = admin_svc.mark_system_initialized(&user.id).await {
// Admin was created but we couldn't mark as initialized.
// This is not fatal — the setup token check prevents re-use, and
// on next restart the system will detect the admin in DB.
tracing::error!(
"Created admin but failed to mark system as initialized: {}",
e
);
}
tracing::info!(
"System initialized: first admin '{}' created successfully",
dto.username
);
Ok((StatusCode::CREATED, Json(user)))
}
/// Constant-time byte comparison to prevent timing attacks on the setup token.
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
2026-02-03 17:59:04 +01:00
/// Get system status - returns whether admin is configured
/// This is a public endpoint used to determine if setup is needed
#[derive(serde::Serialize)]
struct SystemStatus {
/// Whether the system has been set up with an admin
initialized: bool,
/// Number of admin users in the system
admin_count: i64,
/// Whether registration is allowed (only if admin exists)
registration_allowed: bool,
}
async fn get_system_status(
State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, AppError> {
2026-02-14 01:29:34 +01:00
let auth_service = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
2026-02-14 01:29:34 +01:00
// Use the DB flag as the authoritative source for initialization status
let db_initialized = if let Some(admin_svc) = state.admin_settings_service.as_ref() {
admin_svc.is_system_initialized().await
} else {
false
};
// Count admin users for additional info
2026-02-14 01:29:34 +01:00
let admin_count = auth_service
.auth_application_service
.count_admin_users()
.await
2026-02-03 17:59:04 +01:00
.unwrap_or(0);
2026-02-14 01:29:34 +01:00
2026-02-03 17:59:04 +01:00
let status = SystemStatus {
initialized: db_initialized || admin_count > 0,
2026-02-03 17:59:04 +01:00
admin_count,
registration_allowed: db_initialized || admin_count > 0,
2026-02-03 17:59:04 +01:00
};
2026-02-14 01:29:34 +01:00
tracing::info!(
"System status check: initialized={}, admin_count={}",
status.initialized,
status.admin_count
);
2026-02-03 17:59:04 +01:00
Ok((StatusCode::OK, Json(status)))
}
// ============================================================================
// OIDC Handlers
// ============================================================================
/// GET /api/auth/oidc/providers — Returns OIDC provider info for the UI
2026-02-14 01:29:34 +01:00
async fn oidc_providers(State(state): State<Arc<AppState>>) -> Result<impl IntoResponse, AppError> {
let auth_service = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
let auth_app = &auth_service.auth_application_service;
if !auth_app.oidc_enabled() {
return Ok(Json(OidcProviderInfoDto {
enabled: false,
provider_name: String::new(),
authorize_endpoint: String::new(),
password_login_enabled: true,
}));
}
let config = auth_app.oidc_config().unwrap();
Ok(Json(OidcProviderInfoDto {
enabled: true,
provider_name: config.provider_name.clone(),
authorize_endpoint: "/api/auth/oidc/authorize".to_string(),
password_login_enabled: !config.disable_password_login,
}))
}
/// GET /api/auth/oidc/authorize — Redirects user to the OIDC provider
2026-02-14 01:29:34 +01:00
async fn oidc_authorize(State(state): State<Arc<AppState>>) -> Result<impl IntoResponse, AppError> {
let auth_service = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
let auth_app = &auth_service.auth_application_service;
if !auth_app.oidc_enabled() {
return Err(AppError::new(
StatusCode::NOT_FOUND,
"OIDC is not enabled",
"OidcDisabled",
));
}
// Prepare OIDC authorization flow (generates CSRF state, PKCE pair, nonce)
let authorize_url = auth_app.prepare_oidc_authorize().await?;
tracing::info!("OIDC authorize redirect generated");
Ok(Redirect::temporary(&authorize_url))
}
/// GET /api/auth/oidc/callback?code=...&state=... — Handles OIDC callback
async fn oidc_callback(
State(state): State<Arc<AppState>>,
Query(query): Query<OidcCallbackQueryDto>,
) -> Result<impl IntoResponse, AppError> {
2026-02-14 01:29:34 +01:00
let auth_service = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
let auth_app = &auth_service.auth_application_service;
if !auth_app.oidc_enabled() {
return Err(AppError::new(
StatusCode::NOT_FOUND,
"OIDC is not enabled",
"OidcDisabled",
));
}
tracing::info!("OIDC callback received with code");
// Exchange code, validate state/nonce/PKCE, authenticate user
2026-02-14 01:29:34 +01:00
let exchange_code = auth_app
.oidc_callback(&query.code, &query.state)
.await
.map_err(|e| {
tracing::error!("OIDC callback failed: {}", e);
AppError::from(e)
})?;
// Redirect to frontend with one-time exchange code (NOT raw tokens)
let config = auth_app.oidc_config().unwrap();
let frontend_url = config.frontend_url.trim_end_matches('/');
2026-02-14 01:29:34 +01:00
let redirect_url = format!("{}/?oidc_code={}", frontend_url, exchange_code,);
tracing::info!("OIDC login successful, redirecting with exchange code");
Ok(Redirect::temporary(&redirect_url))
}
/// POST /api/auth/oidc/exchange — Exchange one-time code for auth tokens
/// Request body: { "code": "<one_time_code>" }
async fn oidc_exchange(
State(state): State<Arc<AppState>>,
Json(body): Json<OidcExchangeDto>,
) -> Result<Response, AppError> {
2026-02-14 01:29:34 +01:00
let auth_service = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
2026-02-14 01:29:34 +01:00
let auth_response = auth_service
.auth_application_service
.exchange_oidc_token(&body.code)
.map_err(|e| {
tracing::warn!("OIDC token exchange failed: {}", e);
AppError::from(e)
})?;
2026-02-14 01:29:34 +01:00
tracing::info!(
"OIDC token exchange successful for user: {}",
auth_response.user.username
);
// Set HttpOnly cookies for the browser
let mut response = (StatusCode::OK, Json(&auth_response)).into_response();
cookie_auth::append_auth_cookies(
response.headers_mut(),
&auth_response.access_token,
&auth_response.refresh_token,
auth_response.expires_in,
state.core.config.auth.refresh_token_expiry_secs,
);
2026-03-03 01:49:18 +01:00
cookie_auth::append_csrf_cookie(response.headers_mut(), auth_response.expires_in);
Ok(response)
}