MatrixGW/src/extractors/client_auth.rs

137 lines
4.6 KiB
Rust

use crate::user::{APIClient, APIClientID, UserConfig, UserID};
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
use jwt_simple::common::VerificationOptions;
use jwt_simple::prelude::{Duration, HS256Key, MACLike};
use std::str::FromStr;
pub struct APIClientAuth {
pub user: UserConfig,
client: APIClient,
payload: Option<Vec<u8>>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct TokenClaims {
pub method: String,
pub uri: String,
}
impl APIClientAuth {
async fn extract_auth(req: &HttpRequest) -> Result<Self, actix_web::Error> {
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 mut verif = VerificationOptions::default();
verif.max_validity = Some(Duration::from_mins(15));
let claims = match key.verify_token::<TokenClaims>(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 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!",
));
}
// TODO : handle payload
// TODO : update last use (if required)
// TODO : check for IP restriction
Ok(Self {
client: client.clone(),
payload: None,
user,
})
}
}
impl FromRequest for APIClientAuth {
type Error = actix_web::Error;
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let req = req.clone();
Box::pin(async move { Self::extract_auth(&req).await })
}
}