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