Verify API auth token
This commit is contained in:
		
							
								
								
									
										113
									
								
								src/extractors/client_auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/extractors/client_auth.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1
									
								
								src/extractors/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
pub mod client_auth;
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
pub mod app_config;
 | 
			
		||||
pub mod constants;
 | 
			
		||||
pub mod extractors;
 | 
			
		||||
pub mod server;
 | 
			
		||||
pub mod user;
 | 
			
		||||
pub mod utils;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										8
									
								
								src/server/api.rs
									
									
									
									
									
										Normal 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)))
 | 
			
		||||
}
 | 
			
		||||
@@ -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)]
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								src/user.rs
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								src/user.rs
									
									
									
									
									
								
							@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user