#http-request #security #http-client #web #http-response #api-client

mauth-client

Sign requests and validate responses using the Medidata MAuth protocol

6 releases (breaking)

new 0.6.0 Jan 7, 2025
0.5.0 Dec 10, 2024
0.4.0 Jun 11, 2024
0.3.0 Jan 4, 2024
0.1.0 Jan 19, 2021

#981 in Web programming

Download history 12/week @ 2024-09-21 4/week @ 2024-09-28 1/week @ 2024-10-05 155/week @ 2024-12-07 26/week @ 2024-12-14 104/week @ 2025-01-04

135 downloads per month

MIT license

53KB
862 lines

mauth-client

This crate allows users of the Reqwest crate for making HTTP requests to sign those requests with the MAuth protocol, and verify the responses. Usage example:

Note: This crate and Rust support within Medidata is considered experimental. Do not release any code to Production or deploy in a Client-accessible environment without getting approval for the full stack used through the Architecture and Security groups.

Outgoing Requests

use mauth_client::MAuthInfo;
use reqwest::Client;
# async fn send_request() {
let mauth_info = MAuthInfo::from_default_file().unwrap();
let client = Client::new();
let mut req = client.get("https://www.example.com/").build().unwrap();
mauth_info.sign_request(&mut req);
match client.execute(req).await {
    Err(err) => println!("Got error {}", err),
    Ok(response) => println!("Got validated response with body {}", response.text().await.unwrap()),
}
# }

The above code will read your mauth configuration from a file in ~/.mauth_config.yml which format is:

common: &common
  mauth_baseurl: https://<URL of MAUTH SERVER>
  mauth_api_version: v1
  app_uuid: <YOUR APP UUID HERE>
  private_key_file: <PATH TO MAUTH KEY>

The MAuthInfo struct also functions as a outgoing middleware using the reqwest-middleware crate for a simpler API and easier integration with other outgoing middleware:

use mauth_client::MAuthInfo;
use reqwest::Client;
use reqwest_middleware::ClientBuilder;
# async fn send_request() {
let mauth_info = MAuthInfo::from_default_file().unwrap();
let client = ClientBuilder::new(Client::new()).with(mauth_info).build();
match client.get("https://www.example.com/").send().await {
    Err(err) => println!("Got error {}", err),
    Ok(response) => println!("Got validated response with body {}", response.text().await.unwrap()),
}
# }

Incoming Requests

The optional axum-service feature provides for a Tower Layer and Service that will authenticate incoming requests via MAuth V2 or V1 and provide to the lower layers a validated app_uuid from the request via the ValidatedRequestDetails struct. Note that this feature now includes a RequiredMAuthValidationLayer, which will reject any requests without a valid signature before they reach lower layers, and also a OptionalMAuthValidationLayer, which lets all requests through, but only attaches a ValidatedRequestDetails extension struct if there is a valid signature. When using this layer, it is the responsiblity of the request handler to check for the extension and reject requests that are not properly authorized.

Note that ValidatedRequestDetails implements Axum's FromRequestParts, so you can specify it bare in a request handler. This implementation includes returning a 401 Unauthorized status code if the extension is not present. If you would like to return a different response, or respond to the lack of the extension in another way, you can use a more manual mechanism to check for the extension and decide how to proceed if it is not present.

Examples for RequiredMAuthValidationLayer

# async fn run_server() {
use mauth_client::{
    axum_service::RequiredMAuthValidationLayer,
    validate_incoming::ValidatedRequestDetails,
};
use axum::{http::StatusCode, Router, routing::get, serve};
use tokio::net::TcpListener;

// If there is not a valid mauth signature, this function will never run at all, and
// the request will return an empty 401 Unauthorized
async fn foo() -> StatusCode {
    StatusCode::OK
}

// In addition to returning a 401 Unauthorized without running if there is not a valid
// MAuth signature, this also makes the validated requesting app UUID available to
// the function
async fn bar(details: ValidatedRequestDetails) -> StatusCode {
    println!("Got a request from app with UUID: {}", details.app_uuid);
    StatusCode::OK
}

// This function will run regardless of whether or not there is a mauth signature
async fn baz() -> StatusCode {
    StatusCode::OK
}

// Attaching the baz route handler after the layer means the layer is not run for
// requests to that path, so no mauth checking will be performed for that route and
// any other routes attached after the layer
let router = Router::new()
    .route("/foo", get(foo))
    .route("/bar", get(bar))
    .layer(RequiredMAuthValidationLayer::from_default_file().unwrap())
    .route("/baz", get(baz));
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
serve(listener, router).await.unwrap();
# }

Examples for OptionalMAuthValidationLayer

# async fn run_server() {
use mauth_client::{
    axum_service::OptionalMAuthValidationLayer,
    validate_incoming::ValidatedRequestDetails,
};
use axum::{http::StatusCode, Router, routing::get, serve};
use tokio::net::TcpListener;

// This request will run no matter what the authorization status is
async fn foo() -> StatusCode {
    StatusCode::OK
}

// If there is not a valid mauth signature, this function will never run at all, and
// the request will return an empty 401 Unauthorized
async fn bar(_: ValidatedRequestDetails) -> StatusCode {
    StatusCode::OK
}

// In addition to returning a 401 Unauthorized without running if there is not a valid
// MAuth signature, this also makes the validated requesting app UUID available to
// the function
async fn baz(details: ValidatedRequestDetails) -> StatusCode {
    println!("Got a request from app with UUID: {}", details.app_uuid);
    StatusCode::OK
}

// This request will run whether or not there is a valid mauth signature, but the Option
// provided can be used to tell you whether there was a valid signature, so you can
// implement things like multiple possible types of authentication or behavior other than
// a 401 return if there is no authentication
async fn bam(optional_details: Option<ValidatedRequestDetails>) -> StatusCode {
    match optional_details {
        Some(details) => println!("Got a request from app with UUID: {}", details.app_uuid),
        None => println!("Got a request without a valid mauth signature"),
    }
    StatusCode::OK
}

let router = Router::new()
    .route("/foo", get(foo))
    .route("/bar", get(bar))
    .route("/baz", get(baz))
    .route("/bam", get(bam))
    .layer(OptionalMAuthValidationLayer::from_default_file().unwrap());
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
serve(listener, router).await.unwrap();
# }

Error Handling

Both the RequiredMAuthValidationLayer and the OptionalMAuthValidationLayer layers will log errors encountered via tracing under the mauth_client::validate_incoming target.

The Required layer returns the 401 response immediately, so there is no convenient way to retrieve the error in order to do anything more sophisticated with it.

The Optional layer, in addition to loging the error, will also add the MAuthValidationError to the request extensions. If desired, any request handlers or middlewares can retrieve it from there in order to take further actions based on the error type. This error type also implements Axum's OptionalFromRequestParts, so you can more easily retrieve it using Option<MAuthValidationError> anywhere that supports extractors.

OpenTelemetry Integration

There are also optional features tracing-otel-26 and tracing-otel-27 that pair with the axum-service feature to ensure that any outgoing requests for credentials that take place in the context of an incoming web request also include the proper OpenTelemetry span information in any requests to MAudit services. Note that it is critical to use the same version of OpenTelemetry crates as the rest of the project - if you do not, there will be 2 or more instances of the OpenTelemetry global information, and requests may not be traced through properly.

Dependencies

~18–38MB
~550K SLoC