Verify API auth token

This commit is contained in:
2025-01-29 21:50:17 +01:00
parent b92149a77d
commit 6874aebfc7
12 changed files with 586 additions and 18 deletions

View File

@ -0,0 +1,113 @@
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::{HS256Key, MACLike};
use std::str::FromStr;
pub struct APIClientAuth {
pub user: UserConfig,
client: APIClient,
payload: Option<Vec<u8>>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct JWTClaims {}
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 claims =
match key.verify_token::<JWTClaims>(jwt_token, Some(VerificationOptions::default())) {
Ok(t) => t,
Err(e) => {
log::error!("JWT validation failed! {e}");
return Err(actix_web::error::ErrorForbidden("JWT validation failed!"));
}
};
// TODO : check timing
// TODO : check URI & verb
// TODO : handle payload
// TODO : check read only access
// 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 })
}
}

1
src/extractors/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod client_auth;

View File

@ -1,5 +1,6 @@
pub mod app_config;
pub mod constants;
pub mod extractors;
pub mod server;
pub mod user;
pub mod utils;

View File

@ -3,7 +3,7 @@ use actix_session::{storage::RedisSessionStore, SessionMiddleware};
use actix_web::cookie::Key;
use actix_web::{web, App, HttpServer};
use matrix_gateway::app_config::AppConfig;
use matrix_gateway::server::web_ui;
use matrix_gateway::server::{api, web_ui};
use matrix_gateway::user::UserConfig;
#[actix_web::main]
@ -41,9 +41,8 @@ async fn main() -> std::io::Result<()> {
.route("/", web::post().to(web_ui::home))
.route("/oidc_cb", web::get().to(web_ui::oidc_cb))
.route("/sign_out", web::get().to(web_ui::sign_out))
// API routes
// TODO
// API routes
.route("/api/", web::get().to(api::api_home))
})
.bind(&AppConfig::get().listen_address)?
.run()

8
src/server/api.rs Normal file
View File

@ -0,0 +1,8 @@
use crate::extractors::client_auth::APIClientAuth;
use crate::server::HttpResult;
use actix_web::HttpResponse;
/// API Home route
pub async fn api_home(auth: APIClientAuth) -> HttpResult {
Ok(HttpResponse::Ok().body(format!("Welcome user {}!", auth.user.user_id.0)))
}

View File

@ -2,6 +2,7 @@ use actix_web::http::StatusCode;
use actix_web::{HttpResponse, ResponseError};
use std::error::Error;
pub mod api;
pub mod web_ui;
#[derive(thiserror::Error, Debug)]

View File

@ -1,7 +1,7 @@
use crate::app_config::AppConfig;
use crate::constants::{STATE_KEY, USER_SESSION_KEY};
use crate::server::{HttpFailure, HttpResult};
use crate::user::{APIClient, User, UserConfig, UserID};
use crate::user::{APIClient, APIClientID, User, UserConfig, UserID};
use crate::utils;
use actix_session::Session;
use actix_web::{web, HttpResponse};
@ -33,6 +33,7 @@ pub async fn static_file(path: web::Path<String>) -> HttpResult {
#[template(path = "index.html")]
struct HomeTemplate {
name: String,
user_id: UserID,
matrix_token: String,
clients: Vec<APIClient>,
success_message: Option<String>,
@ -55,7 +56,7 @@ pub struct FormRequest {
readonly_client: Option<String>,
/// Delete a specified client id
delete_client_id: Option<uuid::Uuid>,
delete_client_id: Option<APIClientID>,
}
/// Main route
@ -82,7 +83,7 @@ pub async fn home(session: Session, form_req: Option<web::Form<FormRequest>>) ->
let mut error_message = None;
// Retrieve user configuration
let mut config = UserConfig::load(&user.id)
let mut config = UserConfig::load(&user.id, true)
.await
.map_err(HttpFailure::FetchUserConfig)?;
@ -137,6 +138,7 @@ pub async fn home(session: Session, form_req: Option<web::Form<FormRequest>>) ->
.body(
HomeTemplate {
name: user.name,
user_id: user.id,
matrix_token: config.obfuscated_matrix_token(),
clients: config.clients,
success_message,

View File

@ -1,6 +1,7 @@
use s3::error::S3Error;
use s3::request::ResponseData;
use s3::{Bucket, BucketConfiguration};
use std::str::FromStr;
use thiserror::Error;
use crate::app_config::AppConfig;
@ -29,11 +30,28 @@ pub struct User {
pub email: String,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct APIClientID(pub uuid::Uuid);
impl APIClientID {
pub fn generate() -> Self {
Self(uuid::Uuid::new_v4())
}
}
impl FromStr for APIClientID {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(uuid::Uuid::from_str(s)?))
}
}
/// Single API client information
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct APIClient {
/// Client unique ID
pub id: uuid::Uuid,
pub id: APIClientID,
/// Client description
pub description: String,
@ -68,7 +86,7 @@ impl APIClient {
/// Generate a new API client
pub fn generate(description: String, network: Option<ipnet::IpNet>) -> Self {
Self {
id: uuid::Uuid::new_v4(),
id: APIClientID::generate(),
description,
network,
secret: rand_str(TOKEN_LEN),
@ -137,15 +155,15 @@ impl UserConfig {
}
/// Get current user configuration
pub async fn load(user_id: &UserID) -> anyhow::Result<Self> {
pub async fn load(user_id: &UserID, allow_non_existing: bool) -> anyhow::Result<Self> {
let res: Result<ResponseData, S3Error> = AppConfig::get()
.s3_bucket()?
.get_object(user_id.conf_path_in_bucket())
.await;
match res {
Ok(res) => Ok(serde_json::from_slice(res.as_slice())?),
Err(S3Error::HttpFailWithBody(404, _)) => {
match (res, allow_non_existing) {
(Ok(res), _) => Ok(serde_json::from_slice(res.as_slice())?),
(Err(S3Error::HttpFailWithBody(404, _)), true) => {
log::warn!("User configuration does not exists, generating a new one...");
Ok(Self {
user_id: user_id.clone(),
@ -155,7 +173,7 @@ impl UserConfig {
clients: vec![],
})
}
Err(e) => Err(UserError::FetchUserConfig(e).into()),
(Err(e), _) => Err(UserError::FetchUserConfig(e).into()),
}
}
@ -188,4 +206,9 @@ impl UserConfig {
})
.collect()
}
/// Find a client by its id
pub fn find_client_by_id(&self, id: &APIClientID) -> Option<&APIClient> {
self.clients.iter().find(|c| &c.id == id)
}
}