use crate::app_config::AppConfig; use crate::constants; use crate::extractors::money_session::MoneySession; use crate::models::tokens::{Token, TokenID}; use crate::models::users::{User, UserID}; use crate::services::{tokens_service, users_service}; use actix_remote_ip::RemoteIP; use actix_web::dev::Payload; use actix_web::error::ErrorPreconditionFailed; use actix_web::{Error, FromRequest, HttpRequest}; use jwt_simple::algorithms::{HS256Key, MACLike}; use jwt_simple::common::VerificationOptions; use jwt_simple::prelude::Duration; use std::str::FromStr; #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct TokenClaims { #[serde(rename = "met")] pub method: String, pub uri: String, } #[derive(Debug, Clone)] pub enum AuthenticatedMethod { /// User is authenticated using a cookie Cookie, /// User is authenticated through command line, for debugging purposes only Dev, /// User is authenticated using an API token Token(Token), } /// Authentication extractor. Extract authentication information from request pub struct AuthExtractor { pub method: AuthenticatedMethod, pub user: User, } impl AuthExtractor { /// Get current user ID pub fn user_id(&self) -> UserID { self.user.id() } } impl FromRequest for AuthExtractor { type Error = Error; type Future = futures_util::future::LocalBoxFuture<'static, Result>; fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let req = req.clone(); let remote_ip = match RemoteIP::from_request(&req, &mut Payload::None).into_inner() { Ok(ip) => ip, Err(e) => return Box::pin(async { Err(e) }), }; Box::pin(async move { // Check for authentication using OpenID if let Some(token) = req.headers().get(constants::API_TOKEN_HEADER) { let Ok(jwt_token) = token.to_str() else { return Err(actix_web::error::ErrorBadRequest( "Failed to decode token as string!", )); }; let metadata = match jwt_simple::token::Token::decode_metadata(jwt_token) { Ok(m) => m, Err(e) => { log::error!("Failed to decode JWT header metadata! {e}"); return Err(actix_web::error::ErrorBadRequest( "Failed to decode JWT header metadata!", )); } }; // Extract token ID let Some(kid) = metadata.key_id() else { return Err(actix_web::error::ErrorBadRequest( "Missing key id in request!", )); }; let token_id = match TokenID::from_str(kid) { Ok(i) => i, Err(e) => { log::error!("Failed to parse token id! {e}"); return Err(actix_web::error::ErrorBadRequest( "Failed to parse token id!", )); } }; // Get token information let Ok(token) = tokens_service::get_by_id(token_id).await else { log::error!("Token not found!"); return Err(actix_web::error::ErrorForbidden("Token not found!")); }; // Decode JWT let key = HS256Key::from_bytes(token.token_value.as_ref()); let verif = VerificationOptions { max_validity: Some(Duration::from_secs(constants::API_TOKEN_JWT_MAX_DURATION)), ..Default::default() }; let claims = match key.verify_token::(jwt_token, Some(verif)) { Ok(t) => t, Err(e) => { log::error!("JWT validation failed! {e}"); return Err(actix_web::error::ErrorForbidden("JWT validation failed!")); } }; // Check for nonce if claims.nonce.is_none() { return Err(actix_web::error::ErrorBadRequest( "A nonce is required in auth JWT!", )); } // Check IP restriction if let Some(net) = token.ip_net() { if !net.contains(&remote_ip.0) { log::error!( "Trying to use token {:?} from unauthorized IP address: {remote_ip:?}", token.id() ); return Err(actix_web::error::ErrorForbidden( "This token cannot be used from this IP address!", )); } } // Check for write access if token.read_only && !req.method().is_safe() { return Err(actix_web::error::ErrorBadRequest( "Read only token cannot perform write operations!", )); } // Check for authorization let uri = req.uri().to_string(); let authorized = (uri.starts_with("/api/account") && token.right_account) || (uri.starts_with("/api/movement") && token.right_movement) || (uri.starts_with("/api/inbox") && token.right_inbox) || (uri.starts_with("/api/file") && token.right_file) || (uri.starts_with("/api/auth/") && token.right_auth) || (uri.starts_with("/api/stats") && token.right_stats) || (uri.starts_with("/api/backup") && token.right_backup); if !authorized { return Err(actix_web::error::ErrorBadRequest( "This token cannot be used to query this route!", )); } // Get user information let Ok(user) = users_service::get_user_by_id(token.user_id()) else { return Err(actix_web::error::ErrorBadRequest( "Failed to get user information from token!", )); }; // Update last use (if needed) if token.shall_update_time_used() { if let Err(e) = tokens_service::update_time_used(&token).await { log::error!("Failed to refresh last usage of token! {}", e); } } // Handle tokens expiration if token.is_expired() { log::error!("Attempted to use expired token! {:?}", token); return Err(actix_web::error::ErrorBadRequest("Token has expired!")); } return Ok(Self { method: AuthenticatedMethod::Token(token), user, }); } // Check if login is hard-coded as program argument if let Some(email) = &AppConfig::get().unsecure_auto_login_email { let user = users_service::get_user_by_email(email).map_err(|e| { log::error!("Failed to retrieve dev user: {e}"); ErrorPreconditionFailed("Unable to retrieve dev user!") })?; return Ok(Self { method: AuthenticatedMethod::Dev, user, }); } // Check for cookie authentication let session = MoneySession::extract(&req).await?; if let Some(user_id) = session.current_user().map_err(|e| { log::error!("Failed to retrieve user id: {e}"); ErrorPreconditionFailed("Failed to read session information!") })? { let user = users_service::get_user_by_id(user_id).map_err(|e| { log::error!("Failed to retrieve user from cookie session: {e}"); ErrorPreconditionFailed("Failed to retrieve user information!") })?; return Ok(Self { method: AuthenticatedMethod::Cookie, user, }); }; Err(ErrorPreconditionFailed("Authentication required!")) }) } }