Skip to content

Commit

Permalink
Merge pull request #21 from mdsol/optional-signature-middleware
Browse files Browse the repository at this point in the history
Optional signature middleware
  • Loading branch information
masongup-mdsol authored Jan 7, 2025
2 parents 18fe493 + d613f71 commit 5df1848
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 36 deletions.
9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mauth-client"
version = "0.5.0"
version = "0.6.0"
authors = ["Mason Gup <mgup@mdsol.com>"]
edition = "2021"
documentation = "https://docs.rs/mauth-client/"
Expand All @@ -26,17 +26,18 @@ dirs = "5"
chrono = "0.4"
tokio = { version = "1", features = ["fs"] }
tower = { version = "0.4", optional = true }
axum = { version = ">= 0.7.2", optional = true }
axum = { version = ">= 0.8", optional = true }
futures-core = { version = "0.3", optional = true }
http = "1"
bytes = { version = "1", optional = true }
thiserror = "1"
mauth-core = "0.5"
mauth-core = "0.6"
tracing = { version = "0.1", optional = true }

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

[features]
axum-service = ["tower", "futures-core", "axum", "bytes"]
axum-service = ["tower", "futures-core", "axum", "bytes", "tracing"]
tracing-otel-26 = ["reqwest-tracing/opentelemetry_0_26"]
tracing-otel-27 = ["reqwest-tracing/opentelemetry_0_27"]
131 changes: 130 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ the MAuth protocol, and verify the responses. Usage example:
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

```no_run
use mauth_client::MAuthInfo;
use reqwest::Client;
Expand Down Expand Up @@ -49,9 +51,136 @@ match client.get("https://www.example.com/").send().await {
# }
```

## 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.
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`

```no_run
# 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`

```no_run
# 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
Expand Down
Loading

0 comments on commit 5df1848

Please sign in to comment.