Skip to content

Commit a5f7e24

Browse files
Merge pull request #19 from mdsol/support-middleware
Support reqwest_middleware and OpenTelemetry
2 parents 6c2abc7 + b69103a commit a5f7e24

File tree

8 files changed

+103
-102
lines changed

8 files changed

+103
-102
lines changed

Cargo.toml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mauth-client"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
authors = ["Mason Gup <mgup@mdsol.com>"]
55
edition = "2021"
66
documentation = "https://docs.rs/mauth-client/"
@@ -14,6 +14,9 @@ categories = ["authentication", "web-programming"]
1414

1515
[dependencies]
1616
reqwest = { version = "0.12", features = ["json"] }
17+
reqwest-middleware = "0.4"
18+
reqwest-tracing = { version = "0.5.5", optional = true }
19+
async-trait = ">= 0.1.83"
1720
url = "2"
1821
serde = { version = "1", features = ["derive"] }
1922
serde_json = "1"
@@ -25,7 +28,7 @@ tokio = { version = "1", features = ["fs"] }
2528
tower = { version = "0.4", optional = true }
2629
axum = { version = ">= 0.7.2", optional = true }
2730
futures-core = { version = "0.3", optional = true }
28-
http = { version = "1", optional = true }
31+
http = "1"
2932
bytes = { version = "1", optional = true }
3033
thiserror = "1"
3134
mauth-core = "0.5"
@@ -34,4 +37,6 @@ mauth-core = "0.5"
3437
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
3538

3639
[features]
37-
axum-service = ["tower", "futures-core", "axum", "http", "bytes"]
40+
axum-service = ["tower", "futures-core", "axum", "bytes"]
41+
tracing-otel-26 = ["reqwest-tracing/opentelemetry_0_26"]
42+
tracing-otel-27 = ["reqwest-tracing/opentelemetry_0_27"]

README.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
# mauth-client
22

3-
## mauth-client
4-
53
This crate allows users of the Reqwest crate for making HTTP requests to sign those requests with
64
the MAuth protocol, and verify the responses. Usage example:
75

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

12-
```rust
10+
```no_run
1311
use mauth_client::MAuthInfo;
1412
use reqwest::Client;
13+
# async fn send_request() {
1514
let mauth_info = MAuthInfo::from_default_file().unwrap();
1615
let client = Client::new();
1716
let mut req = client.get("https://www.example.com/").build().unwrap();
@@ -20,9 +19,9 @@ match client.execute(req).await {
2019
Err(err) => println!("Got error {}", err),
2120
Ok(response) => println!("Got validated response with body {}", response.text().await.unwrap()),
2221
}
22+
# }
2323
```
2424

25-
2625
The above code will read your mauth configuration from a file in `~/.mauth_config.yml` which format is:
2726
```yaml
2827
common: &common
@@ -32,8 +31,32 @@ common: &common
3231
private_key_file: <PATH TO MAUTH KEY>
3332
```
3433
34+
The `MAuthInfo` struct also functions as a outgoing middleware using the
35+
[`reqwest-middleware`](https://crates.io/crates/reqwest-middleware) crate for a simpler API and easier
36+
integration with other outgoing middleware:
37+
38+
```no_run
39+
use mauth_client::MAuthInfo;
40+
use reqwest::Client;
41+
use reqwest_middleware::ClientBuilder;
42+
# async fn send_request() {
43+
let mauth_info = MAuthInfo::from_default_file().unwrap();
44+
let client = ClientBuilder::new(Client::new()).with(mauth_info).build();
45+
match client.get("https://www.example.com/").send().await {
46+
Err(err) => println!("Got error {}", err),
47+
Ok(response) => println!("Got validated response with body {}", response.text().await.unwrap()),
48+
}
49+
# }
50+
```
51+
3552
The optional `axum-service` feature provides for a Tower Layer and Service that will
3653
authenticate incoming requests via MAuth V2 or V1 and provide to the lower layers a
3754
validated app_uuid from the request via the ValidatedRequestDetails struct.
3855

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

src/axum_service.rs

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,9 @@
22
33
use axum::extract::Request;
44
use futures_core::future::BoxFuture;
5-
use mauth_core::verifier::Verifier;
6-
use std::collections::HashMap;
75
use std::error::Error;
8-
use std::sync::{Arc, RwLock};
96
use std::task::{Context, Poll};
107
use tower::{Layer, Service};
11-
use uuid::Uuid;
128

139
use crate::{
1410
config::{ConfigFileSection, ConfigReadError},
@@ -56,11 +52,7 @@ impl<S: Clone> Clone for MAuthValidationService<S> {
5652
fn clone(&self) -> Self {
5753
MAuthValidationService {
5854
// unwrap is safe because we validated the config_info before constructing the layer
59-
mauth_info: MAuthInfo::from_config_section(
60-
&self.config_info,
61-
Some(self.mauth_info.remote_key_store.clone()),
62-
)
63-
.unwrap(),
55+
mauth_info: MAuthInfo::from_config_section(&self.config_info).unwrap(),
6456
config_info: self.config_info.clone(),
6557
service: self.service.clone(),
6658
}
@@ -72,7 +64,6 @@ impl<S: Clone> Clone for MAuthValidationService<S> {
7264
#[derive(Clone)]
7365
pub struct MAuthValidationLayer {
7466
config_info: ConfigFileSection,
75-
remote_key_store: Arc<RwLock<HashMap<Uuid, Verifier>>>,
7667
}
7768

7869
impl<S> Layer<S> for MAuthValidationLayer {
@@ -81,11 +72,7 @@ impl<S> Layer<S> for MAuthValidationLayer {
8172
fn layer(&self, service: S) -> Self::Service {
8273
MAuthValidationService {
8374
// unwrap is safe because we validated the config_info before constructing the layer
84-
mauth_info: MAuthInfo::from_config_section(
85-
&self.config_info,
86-
Some(self.remote_key_store.clone()),
87-
)
88-
.unwrap(),
75+
mauth_info: MAuthInfo::from_config_section(&self.config_info).unwrap(),
8976
config_info: self.config_info.clone(),
9077
service,
9178
}
@@ -97,24 +84,16 @@ impl MAuthValidationLayer {
9784
/// found in the default location.
9885
pub fn from_default_file() -> Result<Self, ConfigReadError> {
9986
let config_info = MAuthInfo::config_section_from_default_file()?;
100-
let remote_key_store = Arc::new(RwLock::new(HashMap::new()));
10187
// Generate a MAuthInfo and then drop it to validate that it works,
10288
// making it safe to use `unwrap` in the service constructor.
103-
MAuthInfo::from_config_section(&config_info, Some(remote_key_store.clone()))?;
104-
Ok(MAuthValidationLayer {
105-
config_info,
106-
remote_key_store,
107-
})
89+
MAuthInfo::from_config_section(&config_info)?;
90+
Ok(MAuthValidationLayer { config_info })
10891
}
10992

11093
/// Construct a MAuthValidationLayer based on the configuration options in a manually
11194
/// created or parsed ConfigFileSection.
11295
pub fn from_config_section(config_info: ConfigFileSection) -> Result<Self, ConfigReadError> {
113-
let remote_key_store = Arc::new(RwLock::new(HashMap::new()));
114-
MAuthInfo::from_config_section(&config_info, Some(remote_key_store.clone()))?;
115-
Ok(MAuthValidationLayer {
116-
config_info,
117-
remote_key_store,
118-
})
96+
MAuthInfo::from_config_section(&config_info)?;
97+
Ok(MAuthValidationLayer { config_info })
11998
}
12099
}

src/config.rs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use crate::MAuthInfo;
2-
use mauth_core::{signer::Signer, verifier::Verifier};
1+
use crate::{MAuthInfo, CLIENT};
2+
use mauth_core::signer::Signer;
3+
use reqwest::Client;
34
use reqwest::Url;
5+
use reqwest_middleware::ClientBuilder;
46
use serde::Deserialize;
5-
use std::collections::HashMap;
67
use std::io;
7-
use std::sync::{Arc, RwLock};
88
use thiserror::Error;
99
use uuid::Uuid;
1010

@@ -15,7 +15,7 @@ impl MAuthInfo {
1515
/// present in the current user's home directory. Returns an enum error type that includes the
1616
/// error types of all crates used.
1717
pub fn from_default_file() -> Result<MAuthInfo, ConfigReadError> {
18-
Self::from_config_section(&Self::config_section_from_default_file()?, None)
18+
Self::from_config_section(&Self::config_section_from_default_file()?)
1919
}
2020

2121
pub(crate) fn config_section_from_default_file() -> Result<ConfigFileSection, ConfigReadError> {
@@ -35,10 +35,7 @@ impl MAuthInfo {
3535
/// Construct the MAuthInfo struct based on a passed-in ConfigFileSection instance. The
3636
/// optional input_keystore is present to support internal cloning and need not be provided
3737
/// if being used outside of the crate.
38-
pub fn from_config_section(
39-
section: &ConfigFileSection,
40-
input_keystore: Option<Arc<RwLock<HashMap<Uuid, Verifier>>>>,
41-
) -> Result<MAuthInfo, ConfigReadError> {
38+
pub fn from_config_section(section: &ConfigFileSection) -> Result<MAuthInfo, ConfigReadError> {
4239
let full_uri: Url = format!(
4340
"{}/mauth/{}/security_tokens/",
4441
&section.mauth_baseurl, &section.mauth_api_version
@@ -55,15 +52,22 @@ impl MAuthInfo {
5552
return Err(ConfigReadError::NoPrivateKey);
5653
}
5754

58-
Ok(MAuthInfo {
55+
let mauth_info = MAuthInfo {
5956
app_id: Uuid::parse_str(&section.app_uuid)?,
6057
mauth_uri_base: full_uri,
61-
remote_key_store: input_keystore
62-
.unwrap_or_else(|| Arc::new(RwLock::new(HashMap::new()))),
6358
sign_with_v1_also: !section.v2_only_sign_requests.unwrap_or(false),
6459
allow_v1_auth: !section.v2_only_authenticate.unwrap_or(false),
6560
signer: Signer::new(section.app_uuid.clone(), pk_data.unwrap())?,
66-
})
61+
};
62+
63+
CLIENT.get_or_init(|| {
64+
let builder = ClientBuilder::new(Client::new()).with(mauth_info.clone());
65+
#[cfg(any(feature = "tracing-otel-26", feature = "tracing-otel-27"))]
66+
let builder = builder.with(reqwest_tracing::TracingMiddleware::default());
67+
builder.build()
68+
});
69+
70+
Ok(mauth_info)
6771
}
6872
}
6973

@@ -145,7 +149,7 @@ mod test {
145149
v2_only_sign_requests: None,
146150
v2_only_authenticate: None,
147151
};
148-
let load_result = MAuthInfo::from_config_section(&bad_config, None);
152+
let load_result = MAuthInfo::from_config_section(&bad_config);
149153
assert!(matches!(load_result, Err(ConfigReadError::InvalidUri(_))));
150154
}
151155

@@ -160,7 +164,7 @@ mod test {
160164
v2_only_sign_requests: None,
161165
v2_only_authenticate: None,
162166
};
163-
let load_result = MAuthInfo::from_config_section(&bad_config, None);
167+
let load_result = MAuthInfo::from_config_section(&bad_config);
164168
assert!(matches!(
165169
load_result,
166170
Err(ConfigReadError::FileReadError(_))
@@ -180,7 +184,7 @@ mod test {
180184
v2_only_sign_requests: None,
181185
v2_only_authenticate: None,
182186
};
183-
let load_result = MAuthInfo::from_config_section(&bad_config, None);
187+
let load_result = MAuthInfo::from_config_section(&bad_config);
184188
fs::remove_file(&filename).await.unwrap();
185189
assert!(matches!(
186190
load_result,
@@ -201,7 +205,7 @@ mod test {
201205
v2_only_sign_requests: None,
202206
v2_only_authenticate: None,
203207
};
204-
let load_result = MAuthInfo::from_config_section(&bad_config, None);
208+
let load_result = MAuthInfo::from_config_section(&bad_config);
205209
fs::remove_file(&filename).await.unwrap();
206210
assert!(matches!(
207211
load_result,

src/lib.rs

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,41 @@
11
#![forbid(unsafe_code)]
2-
//! # mauth-client
3-
//!
4-
//! This crate allows users of the Reqwest crate for making HTTP requests to sign those requests with
5-
//! the MAuth protocol, and verify the responses. Usage example:
6-
//!
7-
//! **Note**: This crate and Rust support within Medidata is considered experimental. Do not
8-
//! release any code to Production or deploy in a Client-accessible environment without getting
9-
//! approval for the full stack used through the Architecture and Security groups.
10-
//!
11-
//! ```no_run
12-
//! use mauth_client::MAuthInfo;
13-
//! use reqwest::Client;
14-
//! # async fn make_signed_request() {
15-
//! let mauth_info = MAuthInfo::from_default_file().unwrap();
16-
//! let client = Client::new();
17-
//! let mut req = client.get("https://www.example.com/").build().unwrap();
18-
//! mauth_info.sign_request(&mut req);
19-
//! match client.execute(req).await {
20-
//! Err(err) => println!("Got error {}", err),
21-
//! Ok(response) => println!("Got validated response with body {}", response.text().await.unwrap()),
22-
//! }
23-
//! # }
24-
//! ```
25-
//!
26-
//!
27-
//! The above code will read your mauth configuration from a file in `~/.mauth_config.yml` which format is:
28-
//! ```yaml
29-
//! common: &common
30-
//! mauth_baseurl: https://<URL of MAUTH SERVER>
31-
//! mauth_api_version: v1
32-
//! app_uuid: <YOUR APP UUID HERE>
33-
//! private_key_file: <PATH TO MAUTH KEY>
34-
//! ```
35-
//!
36-
//! The optional `axum-service` feature provides for a Tower Layer and Service that will
37-
//! authenticate incoming requests via MAuth V2 or V1 and provide to the lower layers a
38-
//! validated app_uuid from the request via the ValidatedRequestDetails struct.
2+
#![doc = include_str!("../README.md")]
393

4+
use ::reqwest_middleware::ClientWithMiddleware;
405
use mauth_core::signer::Signer;
416
use mauth_core::verifier::Verifier;
427
use reqwest::Url;
438
use std::collections::HashMap;
44-
use std::sync::{Arc, RwLock};
9+
use std::sync::{LazyLock, OnceLock, RwLock};
4510
use uuid::Uuid;
4611

4712
/// This is the primary struct of this class. It contains all of the information
4813
/// required to sign requests using the MAuth protocol and verify the responses.
4914
///
5015
/// Note that it contains a cache of response keys for verifying response signatures. This cache
5116
/// makes the struct non-Sync.
52-
#[allow(dead_code)]
17+
#[derive(Clone)]
5318
pub struct MAuthInfo {
5419
app_id: Uuid,
5520
sign_with_v1_also: bool,
5621
signer: Signer,
57-
remote_key_store: Arc<RwLock<HashMap<Uuid, Verifier>>>,
5822
mauth_uri_base: Url,
5923
allow_v1_auth: bool,
6024
}
6125

26+
static CLIENT: OnceLock<ClientWithMiddleware> = OnceLock::new();
27+
28+
static PUBKEY_CACHE: LazyLock<RwLock<HashMap<Uuid, Verifier>>> =
29+
LazyLock::new(|| RwLock::new(HashMap::new()));
30+
6231
/// Tower Service and Layer to allow Tower-integrated servers to validate incoming request
6332
#[cfg(feature = "axum-service")]
6433
pub mod axum_service;
6534
/// Helpers to parse configuration files or supply structs and construct instances of the main struct
6635
pub mod config;
6736
#[cfg(test)]
6837
mod protocol_test_suite;
38+
mod reqwest_middleware;
6939
/// Implementation of code to sign outgoing requests
7040
pub mod sign_outgoing;
7141
/// Implementation of code to validate incoming requests

src/protocol_test_suite.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async fn setup_mauth_info() -> (MAuthInfo, u64) {
3131
v2_only_authenticate: None,
3232
};
3333
(
34-
MAuthInfo::from_config_section(&mock_config_section, None).unwrap(),
34+
MAuthInfo::from_config_section(&mock_config_section).unwrap(),
3535
sign_config.request_time,
3636
)
3737
}

src/reqwest_middleware.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use http::Extensions;
2+
use reqwest::{Request, Response};
3+
use reqwest_middleware::{Middleware, Next, Result};
4+
5+
use crate::{sign_outgoing::SigningError, MAuthInfo};
6+
7+
#[async_trait::async_trait]
8+
impl Middleware for MAuthInfo {
9+
#[must_use]
10+
async fn handle(
11+
&self,
12+
mut req: Request,
13+
extensions: &mut Extensions,
14+
next: Next<'_>,
15+
) -> Result<Response> {
16+
self.sign_request(&mut req)?;
17+
next.run(req, extensions).await
18+
}
19+
}
20+
21+
impl From<SigningError> for reqwest_middleware::Error {
22+
fn from(value: SigningError) -> Self {
23+
reqwest_middleware::Error::Middleware(value.into())
24+
}
25+
}

0 commit comments

Comments
 (0)