use crate::constants::USER_SESSION_KEY; use crate::server::HttpFailure; use crate::user::{APIClient, APIClientID, RumaClient, User, UserConfig, UserID}; use crate::utils::base_utils::curr_time; use actix_remote_ip::RemoteIP; use actix_session::Session; use actix_web::dev::Payload; use actix_web::{FromRequest, HttpRequest}; use bytes::Bytes; use jwt_simple::common::VerificationOptions; use jwt_simple::prelude::{Duration, HS256Key, MACLike}; use ruma::api::{IncomingResponse, OutgoingRequest}; use sha2::{Digest, Sha256}; use std::net::IpAddr; use std::str::FromStr; pub struct APIClientAuth { pub user: UserConfig, pub client: Option, pub payload: Option>, } #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct TokenClaims { #[serde(rename = "met")] pub method: String, pub uri: String, #[serde(rename = "pay", skip_serializing_if = "Option::is_none")] pub payload_sha256: Option, } impl APIClientAuth { async fn extract_auth( req: &HttpRequest, remote_ip: IpAddr, payload_bytes: Option, ) -> Result { // Check if user is authenticated using Web UI let session = Session::from_request(req, &mut Payload::None).await?; if let Some(user) = session.get::(USER_SESSION_KEY)? { match UserConfig::load(&user.id, false).await { Ok(config) => { return Ok(Self { user: config, client: None, payload: payload_bytes.map(|bytes| bytes.to_vec()), }) } Err(e) => { log::error!("Failed to fetch user information for authentication using cookie token! {e}"); } }; } let Some(token) = req.headers().get("x-client-auth") else { return Err(actix_web::error::ErrorBadRequest( "Missing authentication 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!", )); } }; let Some(kid) = metadata.key_id() else { return Err(actix_web::error::ErrorBadRequest( "Missing key id in request!", )); }; let Some((user_id, client_id)) = kid.split_once("#") else { return Err(actix_web::error::ErrorBadRequest( "Invalid key format (missing part)!", )); }; let (Ok(user_id), Ok(client_id)) = (urlencoding::decode(user_id), urlencoding::decode(client_id)) else { return Err(actix_web::error::ErrorBadRequest( "Invalid key format (decoding failed)!", )); }; // Fetch user const USER_NOT_FOUND_ERROR: &str = "User not found!"; let user = match UserConfig::load(&UserID(user_id.to_string()), false).await { Ok(u) => u, Err(e) => { log::error!("Failed to get user information! {e}"); return Err(actix_web::error::ErrorForbidden(USER_NOT_FOUND_ERROR)); } }; // Find client let Ok(client_id) = APIClientID::from_str(&client_id) else { return Err(actix_web::error::ErrorBadRequest("Invalid token format!")); }; let Some(client) = user.find_client_by_id(&client_id) else { log::error!("Client not found for user!"); return Err(actix_web::error::ErrorForbidden(USER_NOT_FOUND_ERROR)); }; // Decode JWT let key = HS256Key::from_bytes(client.secret.as_bytes()); let verif = VerificationOptions { max_validity: Some(Duration::from_mins(15)), ..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 JWT!", )); } // Check IP restriction if let Some(net) = client.network { if !net.contains(&remote_ip) { log::error!( "Trying to use client {} from unauthorized IP address: {remote_ip}", client.id.0 ); return Err(actix_web::error::ErrorForbidden( "This client cannot be used from this IP address!", )); } } // Check URI & verb if claims.custom.uri != req.uri().to_string() { return Err(actix_web::error::ErrorBadRequest("URI mismatch!")); } if claims.custom.method != req.method().to_string() { return Err(actix_web::error::ErrorBadRequest("Method mismatch!")); } // Check for write access if client.readonly_client && !req.method().is_safe() { return Err(actix_web::error::ErrorBadRequest( "Read only client cannot perform write operations!", )); } let payload = match (payload_bytes, claims.custom.payload_sha256) { (None, _) => None, (Some(_), None) => { return Err(actix_web::error::ErrorBadRequest( "A payload digest must be included in the JWT when the request has a payload!", )); } (Some(payload), Some(provided_digest)) => { let computed_digest = base16ct::lower::encode_string(&Sha256::digest(&payload)); if computed_digest != provided_digest { log::error!( "Expected digest {provided_digest} but computed {computed_digest}!" ); return Err(actix_web::error::ErrorBadRequest( "Computed digest is different from the one provided in the JWT!", )); } Some(payload.to_vec()) } }; // Update last use (if needed) if client.need_update_last_used() { let mut user_up = user.clone(); match user_up.find_client_by_id_mut(&client.id) { None => log::error!("Client ID disappeared!!!"), Some(u) => u.used = curr_time().unwrap(), } if let Err(e) = user_up.save().await { log::error!("Failed to update last token usage! {e}"); } } Ok(Self { client: Some(client.clone()), payload, user, }) } /// Get an instance of Matrix client pub async fn client(&self) -> anyhow::Result { self.user.matrix_client().await } /// Send request to matrix server pub async fn send_request, E: IncomingResponse>( &self, request: R, ) -> anyhow::Result { match self.client().await?.send_request(request).await { Ok(e) => Ok(e), Err(e) => Err(HttpFailure::MatrixClientError(e.to_string())), } } } impl FromRequest for APIClientAuth { type Error = actix_web::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) }), }; let mut payload = payload.take(); Box::pin(async move { let payload_bytes = match Bytes::from_request(&req, &mut payload).await { Ok(b) => { if b.is_empty() { None } else { Some(b) } } Err(e) => { log::error!("Failed to extract request payload! {e}"); None } }; Self::extract_auth(&req, remote_ip.0, payload_bytes).await }) } }