257 lines
8.8 KiB
Rust
257 lines
8.8 KiB
Rust
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<APIClient>,
|
|
pub payload: Option<Vec<u8>>,
|
|
}
|
|
|
|
#[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<String>,
|
|
}
|
|
|
|
impl APIClientAuth {
|
|
async fn extract_auth(
|
|
req: &HttpRequest,
|
|
remote_ip: IpAddr,
|
|
payload_bytes: Option<Bytes>,
|
|
) -> Result<Self, actix_web::Error> {
|
|
// 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>(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::<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 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<RumaClient> {
|
|
self.user.matrix_client().await
|
|
}
|
|
|
|
/// Send request to matrix server
|
|
pub async fn send_request<R: OutgoingRequest<IncomingResponse = E>, E: IncomingResponse>(
|
|
&self,
|
|
request: R,
|
|
) -> anyhow::Result<E, HttpFailure> {
|
|
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<Self, Self::Error>>;
|
|
|
|
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
|
|
})
|
|
}
|
|
}
|