137 lines
4.6 KiB
Rust
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 })
|
|
}
|
|
}
|