Basic JWT checks

This commit is contained in:
Pierre HUBERT 2025-01-30 21:33:54 +01:00
parent 6874aebfc7
commit 0e8b4751b4
3 changed files with 112 additions and 13 deletions

81
examples/api_curl.rs Normal file
View File

@ -0,0 +1,81 @@
use clap::Parser;
use jwt_simple::algorithms::HS256Key;
use jwt_simple::prelude::{Clock, Duration, JWTClaims, MACLike};
use matrix_gateway::extractors::client_auth::TokenClaims;
use matrix_gateway::utils::rand_str;
use std::ops::Add;
use std::os::unix::prelude::CommandExt;
use std::process::Command;
/// cURL wrapper to query MatrixGW
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// URL of Matrix GW
#[arg(short('U'), long, env, default_value = "http://localhost:8000")]
matrix_gw_url: String,
/// Token ID
#[arg(short('i'), long, env)]
token_id: String,
/// User ID
#[arg(short('u'), long, env)]
user_id: String,
/// Token secret
#[arg(short('t'), long, env)]
token_secret: String,
/// Request verb
#[arg(short('X'), long, default_value = "GET")]
method: String,
/// Request URI
uri: String,
/// Command line arguments to pass to cURL
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
run: Vec<String>,
}
fn main() {
let args: Args = Args::parse();
let full_url = format!("{}{}", args.matrix_gw_url, args.uri);
log::debug!("Full URL: {full_url}");
let key = HS256Key::from_bytes(args.token_secret.as_bytes());
let claims = JWTClaims::<TokenClaims> {
issued_at: Some(Clock::now_since_epoch()),
expires_at: Some(Clock::now_since_epoch().add(Duration::from_mins(15))),
invalid_before: None,
issuer: None,
subject: None,
audiences: None,
jwt_id: None,
nonce: Some(rand_str(10)),
custom: TokenClaims {
method: args.method.to_string(),
uri: args.uri,
},
};
let jwt = key
.with_key_id(&format!(
"{}#{}",
urlencoding::encode(&args.user_id),
urlencoding::encode(&args.token_id)
))
.authenticate(claims)
.expect("Failed to sign JWT!");
let _ = Command::new("curl")
.args(["-X", &args.method])
.args(["-H", &format!("x-client-auth: {jwt}")])
.args(args.run)
.arg(full_url)
.exec();
panic!("Failed to run curl!")
}

View File

@ -2,7 +2,7 @@ use crate::user::{APIClient, APIClientID, UserConfig, UserID};
use actix_web::dev::Payload; use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest}; use actix_web::{FromRequest, HttpRequest};
use jwt_simple::common::VerificationOptions; use jwt_simple::common::VerificationOptions;
use jwt_simple::prelude::{HS256Key, MACLike}; use jwt_simple::prelude::{Duration, HS256Key, MACLike};
use std::str::FromStr; use std::str::FromStr;
pub struct APIClientAuth { pub struct APIClientAuth {
@ -12,7 +12,10 @@ pub struct APIClientAuth {
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, serde::Serialize, serde::Deserialize)]
struct JWTClaims {} pub struct TokenClaims {
pub method: String,
pub uri: String,
}
impl APIClientAuth { impl APIClientAuth {
async fn extract_auth(req: &HttpRequest) -> Result<Self, actix_web::Error> { async fn extract_auth(req: &HttpRequest) -> Result<Self, actix_web::Error> {
@ -78,17 +81,31 @@ impl APIClientAuth {
// Decode JWT // Decode JWT
let key = HS256Key::from_bytes(client.secret.as_bytes()); let key = HS256Key::from_bytes(client.secret.as_bytes());
let claims = let mut verif = VerificationOptions::default();
match key.verify_token::<JWTClaims>(jwt_token, Some(VerificationOptions::default())) { verif.max_validity = Some(Duration::from_mins(15));
Ok(t) => t, let claims = match key.verify_token::<TokenClaims>(jwt_token, Some(verif)) {
Err(e) => { Ok(t) => t,
log::error!("JWT validation failed! {e}"); Err(e) => {
return Err(actix_web::error::ErrorForbidden("JWT validation failed!")); 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!"));
}
// TODO : check timing
// TODO : check URI & verb
// TODO : handle payload // TODO : handle payload
// TODO : check read only access // TODO : check read only access
// TODO : update last use (if required) // TODO : update last use (if required)

View File

@ -42,7 +42,8 @@ async fn main() -> std::io::Result<()> {
.route("/oidc_cb", web::get().to(web_ui::oidc_cb)) .route("/oidc_cb", web::get().to(web_ui::oidc_cb))
.route("/sign_out", web::get().to(web_ui::sign_out)) .route("/sign_out", web::get().to(web_ui::sign_out))
// API routes // API routes
.route("/api/", web::get().to(api::api_home)) .route("/api", web::get().to(api::api_home))
.route("/api", web::post().to(api::api_home))
}) })
.bind(&AppConfig::get().listen_address)? .bind(&AppConfig::get().listen_address)?
.run() .run()