Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading