Skip to content

Commit 5df1848

Browse files
Merge pull request #21 from mdsol/optional-signature-middleware
Optional signature middleware
2 parents 18fe493 + d613f71 commit 5df1848

File tree

5 files changed

+376
-36
lines changed

5 files changed

+376
-36
lines changed

Cargo.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mauth-client"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
authors = ["Mason Gup <mgup@mdsol.com>"]
55
edition = "2021"
66
documentation = "https://docs.rs/mauth-client/"
@@ -26,17 +26,18 @@ dirs = "5"
2626
chrono = "0.4"
2727
tokio = { version = "1", features = ["fs"] }
2828
tower = { version = "0.4", optional = true }
29-
axum = { version = ">= 0.7.2", optional = true }
29+
axum = { version = ">= 0.8", optional = true }
3030
futures-core = { version = "0.3", optional = true }
3131
http = "1"
3232
bytes = { version = "1", optional = true }
3333
thiserror = "1"
34-
mauth-core = "0.5"
34+
mauth-core = "0.6"
35+
tracing = { version = "0.1", optional = true }
3536

3637
[dev-dependencies]
3738
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
3839

3940
[features]
40-
axum-service = ["tower", "futures-core", "axum", "bytes"]
41+
axum-service = ["tower", "futures-core", "axum", "bytes", "tracing"]
4142
tracing-otel-26 = ["reqwest-tracing/opentelemetry_0_26"]
4243
tracing-otel-27 = ["reqwest-tracing/opentelemetry_0_27"]

README.md

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ the MAuth protocol, and verify the responses. Usage example:
77
release any code to Production or deploy in a Client-accessible environment without getting
88
approval for the full stack used through the Architecture and Security groups.
99

10+
## Outgoing Requests
11+
1012
```no_run
1113
use mauth_client::MAuthInfo;
1214
use reqwest::Client;
@@ -49,9 +51,136 @@ match client.get("https://www.example.com/").send().await {
4951
# }
5052
```
5153

54+
## Incoming Requests
55+
5256
The optional `axum-service` feature provides for a Tower Layer and Service that will
5357
authenticate incoming requests via MAuth V2 or V1 and provide to the lower layers a
54-
validated app_uuid from the request via the ValidatedRequestDetails struct.
58+
validated app_uuid from the request via the `ValidatedRequestDetails` struct. Note that
59+
this feature now includes a `RequiredMAuthValidationLayer`, which will reject any
60+
requests without a valid signature before they reach lower layers, and also a
61+
`OptionalMAuthValidationLayer`, which lets all requests through, but only attaches a
62+
`ValidatedRequestDetails` extension struct if there is a valid signature. When using this
63+
layer, it is the responsiblity of the request handler to check for the extension and
64+
reject requests that are not properly authorized.
65+
66+
Note that `ValidatedRequestDetails` implements Axum's `FromRequestParts`, so you can
67+
specify it bare in a request handler. This implementation includes returning a 401
68+
Unauthorized status code if the extension is not present. If you would like to return
69+
a different response, or respond to the lack of the extension in another way, you can
70+
use a more manual mechanism to check for the extension and decide how to proceed if it
71+
is not present.
72+
73+
### Examples for `RequiredMAuthValidationLayer`
74+
75+
```no_run
76+
# async fn run_server() {
77+
use mauth_client::{
78+
axum_service::RequiredMAuthValidationLayer,
79+
validate_incoming::ValidatedRequestDetails,
80+
};
81+
use axum::{http::StatusCode, Router, routing::get, serve};
82+
use tokio::net::TcpListener;
83+
84+
// If there is not a valid mauth signature, this function will never run at all, and
85+
// the request will return an empty 401 Unauthorized
86+
async fn foo() -> StatusCode {
87+
StatusCode::OK
88+
}
89+
90+
// In addition to returning a 401 Unauthorized without running if there is not a valid
91+
// MAuth signature, this also makes the validated requesting app UUID available to
92+
// the function
93+
async fn bar(details: ValidatedRequestDetails) -> StatusCode {
94+
println!("Got a request from app with UUID: {}", details.app_uuid);
95+
StatusCode::OK
96+
}
97+
98+
// This function will run regardless of whether or not there is a mauth signature
99+
async fn baz() -> StatusCode {
100+
StatusCode::OK
101+
}
102+
103+
// Attaching the baz route handler after the layer means the layer is not run for
104+
// requests to that path, so no mauth checking will be performed for that route and
105+
// any other routes attached after the layer
106+
let router = Router::new()
107+
.route("/foo", get(foo))
108+
.route("/bar", get(bar))
109+
.layer(RequiredMAuthValidationLayer::from_default_file().unwrap())
110+
.route("/baz", get(baz));
111+
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
112+
serve(listener, router).await.unwrap();
113+
# }
114+
```
115+
116+
### Examples for `OptionalMAuthValidationLayer`
117+
118+
```no_run
119+
# async fn run_server() {
120+
use mauth_client::{
121+
axum_service::OptionalMAuthValidationLayer,
122+
validate_incoming::ValidatedRequestDetails,
123+
};
124+
use axum::{http::StatusCode, Router, routing::get, serve};
125+
use tokio::net::TcpListener;
126+
127+
// This request will run no matter what the authorization status is
128+
async fn foo() -> StatusCode {
129+
StatusCode::OK
130+
}
131+
132+
// If there is not a valid mauth signature, this function will never run at all, and
133+
// the request will return an empty 401 Unauthorized
134+
async fn bar(_: ValidatedRequestDetails) -> StatusCode {
135+
StatusCode::OK
136+
}
137+
138+
// In addition to returning a 401 Unauthorized without running if there is not a valid
139+
// MAuth signature, this also makes the validated requesting app UUID available to
140+
// the function
141+
async fn baz(details: ValidatedRequestDetails) -> StatusCode {
142+
println!("Got a request from app with UUID: {}", details.app_uuid);
143+
StatusCode::OK
144+
}
145+
146+
// This request will run whether or not there is a valid mauth signature, but the Option
147+
// provided can be used to tell you whether there was a valid signature, so you can
148+
// implement things like multiple possible types of authentication or behavior other than
149+
// a 401 return if there is no authentication
150+
async fn bam(optional_details: Option<ValidatedRequestDetails>) -> StatusCode {
151+
match optional_details {
152+
Some(details) => println!("Got a request from app with UUID: {}", details.app_uuid),
153+
None => println!("Got a request without a valid mauth signature"),
154+
}
155+
StatusCode::OK
156+
}
157+
158+
let router = Router::new()
159+
.route("/foo", get(foo))
160+
.route("/bar", get(bar))
161+
.route("/baz", get(baz))
162+
.route("/bam", get(bam))
163+
.layer(OptionalMAuthValidationLayer::from_default_file().unwrap());
164+
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
165+
serve(listener, router).await.unwrap();
166+
# }
167+
```
168+
169+
### Error Handling
170+
171+
Both the `RequiredMAuthValidationLayer` and the `OptionalMAuthValidationLayer` layers will
172+
log errors encountered via `tracing` under the `mauth_client::validate_incoming` target.
173+
174+
The Required layer returns the 401 response immediately, so there is no convenient way to
175+
retrieve the error in order to do anything more sophisticated with it.
176+
177+
The Optional layer, in addition to loging the error, will also add the `MAuthValidationError`
178+
to the request extensions. If desired, any request handlers or middlewares can retrieve it
179+
from there in order to take further actions based on the error type. This error type also
180+
implements Axum's `OptionalFromRequestParts`, so you can more easily retrieve it using
181+
`Option<MAuthValidationError>` anywhere that supports extractors.
182+
183+
### OpenTelemetry Integration
55184

56185
There are also optional features `tracing-otel-26` and `tracing-otel-27` that pair with
57186
the `axum-service` feature to ensure that any outgoing requests for credentials that take

0 commit comments

Comments
 (0)