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, header},
|
2026-03-03 01:10:50 +01:00
|
|
|
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,
|
2025-03-20 09:22:31 +01:00
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::common::di::AppState;
|
2026-03-03 01:10:50 +01:00
|
|
|
use crate::interfaces::api::cookie_auth;
|
2026-02-02 23:56:40 +01:00
|
|
|
use crate::interfaces::errors::AppError;
|
2026-03-03 01:10:50 +01:00
|
|
|
use crate::interfaces::middleware::auth::CurrentUserId;
|
2025-03-20 09:22:31 +01:00
|
|
|
|
|
|
|
|
pub fn auth_routes() -> Router<Arc<AppState>> {
|
2026-02-12 09:41:25 +01:00
|
|
|
// Routes that do NOT require authentication
|
2026-02-03 17:59:04 +01:00
|
|
|
let public_routes = Router::new()
|
2025-03-20 09:22:31 +01:00
|
|
|
.route("/register", post(register))
|
|
|
|
|
.route("/login", post(login))
|
|
|
|
|
.route("/refresh", post(refresh_token))
|
2026-02-10 20:32:32 +01:00
|
|
|
.route("/status", get(get_system_status))
|
|
|
|
|
// OIDC endpoints (all public)
|
|
|
|
|
.route("/oidc/providers", get(oidc_providers))
|
|
|
|
|
.route("/oidc/authorize", get(oidc_authorize))
|
2026-02-11 00:37:47 +01:00
|
|
|
.route("/oidc/callback", get(oidc_callback))
|
|
|
|
|
.route("/oidc/exchange", post(oidc_exchange));
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +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
|
|
|
|
2026-02-12 09:41:25 +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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
};
|
2026-02-11 00:37:47 +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()
|
|
|
|
|
{
|
2026-02-11 00:37:47 +01:00
|
|
|
return Err(AppError::new(
|
|
|
|
|
StatusCode::FORBIDDEN,
|
|
|
|
|
"Password registration is disabled. Please use SSO/OIDC to sign in.",
|
|
|
|
|
"PasswordRegistrationDisabled",
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-02-13 16:46:59 +01:00
|
|
|
|
|
|
|
|
// Check if public registration has been disabled by the admin
|
2026-02-14 01:26:02 +01:00
|
|
|
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",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 23:20:46 +01:00
|
|
|
// 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>,
|
2026-03-03 01:10:50 +01:00
|
|
|
) -> 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
|
|
|
}
|
|
|
|
|
};
|
2026-02-10 20:32:32 +01:00
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
|
{
|
2026-02-10 20:32:32 +01:00
|
|
|
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-10 20:32:32 +01:00
|
|
|
));
|
|
|
|
|
}
|
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) => {
|
|
|
|
|
tracing::info!("Login successful for user: {}", dto.username);
|
2025-03-24 16:47:42 +01:00
|
|
|
// Log the response structure for debugging
|
|
|
|
|
tracing::debug!("Auth response: {:?}", &auth_response);
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2025-03-24 16:47:42 +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",
|
|
|
|
|
));
|
2025-03-24 16:47:42 +01:00
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-03 01:10:50 +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,
|
|
|
|
|
);
|
|
|
|
|
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) => {
|
|
|
|
|
tracing::error!("Login failed for user {}: {}", dto.username, err);
|
|
|
|
|
Err(err.into())
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-03 01:10:50 +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>>,
|
2026-03-03 01:10:50 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
body: axum::body::Bytes,
|
|
|
|
|
) -> Result<Response, AppError> {
|
2026-02-08 13:40:23 +01:00
|
|
|
tracing::info!("Token refresh requested");
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
let auth_service = state
|
|
|
|
|
.auth_service
|
|
|
|
|
.as_ref()
|
2026-02-12 09:41:25 +01:00
|
|
|
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-03 01:10:50 +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
|
|
|
|
2026-03-03 01:10:50 +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,
|
|
|
|
|
);
|
|
|
|
|
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>>,
|
2026-03-03 01:10:50 +01:00
|
|
|
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()
|
2026-02-12 09:41:25 +01:00
|
|
|
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +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() {
|
2026-02-12 09:41:25 +01:00
|
|
|
// 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) => {
|
2026-02-12 09:41:25 +01:00
|
|
|
// 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
|
|
|
|
2026-02-12 09:41:25 +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>>,
|
2026-03-03 01:10:50 +01:00
|
|
|
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()
|
2026-02-12 09:41:25 +01:00
|
|
|
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
auth_service
|
|
|
|
|
.auth_application_service
|
2026-03-03 01:10:50 +01:00
|
|
|
.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>>,
|
|
|
|
|
headers: HeaderMap,
|
2026-03-03 01:10:50 +01:00
|
|
|
CurrentUserId(user_id): CurrentUserId,
|
|
|
|
|
) -> Result<Response, AppError> {
|
2026-02-14 01:29:34 +01:00
|
|
|
let auth_service = state
|
|
|
|
|
.auth_service
|
|
|
|
|
.as_ref()
|
2026-02-12 09:41:25 +01:00
|
|
|
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-03 01:10:50 +01:00
|
|
|
// Obtain the raw access token from Bearer header OR cookie
|
2026-02-03 17:59:04 +01:00
|
|
|
let token = headers
|
2025-03-20 09:22:31 +01:00
|
|
|
.get(header::AUTHORIZATION)
|
2026-03-03 01:10:50 +01:00
|
|
|
.and_then(|v| v.to_str().ok())
|
|
|
|
|
.and_then(|v| v.strip_prefix("Bearer "))
|
|
|
|
|
.map(String::from)
|
|
|
|
|
.or_else(|| cookie_auth::extract_cookie_value(&headers, cookie_auth::ACCESS_COOKIE))
|
2026-02-12 09:41:25 +01:00
|
|
|
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
auth_service
|
|
|
|
|
.auth_application_service
|
2026-03-03 01:10:50 +01:00
|
|
|
.logout(&user_id, &token)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await?;
|
|
|
|
|
|
2026-03-03 01:10:50 +01:00
|
|
|
// 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
|
|
|
}
|
|
|
|
|
|
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()
|
2026-02-12 09:41:25 +01:00
|
|
|
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-03 17:59:04 +01:00
|
|
|
// Count admin users to determine if system is initialized
|
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: admin_count > 0,
|
|
|
|
|
admin_count,
|
|
|
|
|
registration_allowed: admin_count > 0, // Only allow registration if admin exists
|
|
|
|
|
};
|
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)))
|
|
|
|
|
}
|
2026-02-10 20:32:32 +01:00
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 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()
|
2026-02-10 20:32:32 +01:00
|
|
|
.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()
|
2026-02-10 20:32:32 +01:00
|
|
|
.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",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 00:37:47 +01:00
|
|
|
// Prepare OIDC authorization flow (generates CSRF state, PKCE pair, nonce)
|
2026-02-13 21:42:41 +01:00
|
|
|
let authorize_url = auth_app.prepare_oidc_authorize().await?;
|
2026-02-10 20:32:32 +01:00
|
|
|
|
|
|
|
|
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()
|
2026-02-10 20:32:32 +01:00
|
|
|
.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");
|
|
|
|
|
|
2026-02-11 00:37:47 +01:00
|
|
|
// 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
|
2026-02-10 20:32:32 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("OIDC callback failed: {}", e);
|
|
|
|
|
AppError::from(e)
|
|
|
|
|
})?;
|
|
|
|
|
|
2026-02-11 00:37:47 +01:00
|
|
|
// Redirect to frontend with one-time exchange code (NOT raw tokens)
|
2026-02-10 20:32:32 +01:00
|
|
|
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,);
|
2026-02-10 20:32:32 +01:00
|
|
|
|
2026-02-11 00:37:47 +01:00
|
|
|
tracing::info!("OIDC login successful, redirecting with exchange code");
|
2026-02-10 20:32:32 +01:00
|
|
|
|
|
|
|
|
Ok(Redirect::temporary(&redirect_url))
|
|
|
|
|
}
|
2026-02-11 00:37:47 +01:00
|
|
|
|
|
|
|
|
/// 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>,
|
2026-03-03 01:10:50 +01:00
|
|
|
) -> Result<Response, AppError> {
|
2026-02-14 01:29:34 +01:00
|
|
|
let auth_service = state
|
|
|
|
|
.auth_service
|
|
|
|
|
.as_ref()
|
2026-02-11 00:37:47 +01:00
|
|
|
.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
|
2026-02-11 00:37:47 +01:00
|
|
|
.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
|
|
|
|
|
);
|
2026-02-11 00:37:47 +01:00
|
|
|
|
2026-03-03 01:10:50 +01:00
|
|
|
// 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,
|
|
|
|
|
);
|
|
|
|
|
cookie_auth::append_csrf_cookie(
|
|
|
|
|
response.headers_mut(),
|
|
|
|
|
auth_response.expires_in,
|
|
|
|
|
);
|
|
|
|
|
Ok(response)
|
2026-02-11 00:37:47 +01:00
|
|
|
}
|