diff --git a/tower-http/CHANGELOG.md b/tower-http/CHANGELOG.md index d8e8cf56..0f6e8d3f 100644 --- a/tower-http/CHANGELOG.md +++ b/tower-http/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased +- Add `AddAuthorizationLayer` for setting the `Authorization` header on + requests. - Add example of using `SharedClassifier`. - Add `StatusInRangeAsFailures` which is a response classifier that considers responses with status code in a certain range as failures. Useful for HTTP diff --git a/tower-http/src/auth/add_authorization.rs b/tower-http/src/auth/add_authorization.rs new file mode 100644 index 00000000..202c42d9 --- /dev/null +++ b/tower-http/src/auth/add_authorization.rs @@ -0,0 +1,262 @@ +//! Add authorization to requests using the [`Authorization`] header. +//! +//! [`Authorization`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization +//! +//! # Example +//! +//! ``` +//! use tower_http::auth::AddAuthorizationLayer; +//! use hyper::{Request, Response, Body, Error}; +//! use http::{StatusCode, header::AUTHORIZATION}; +//! use tower::{Service, ServiceExt, ServiceBuilder, service_fn}; +//! # async fn handle(request: Request) -> Result, Error> { +//! # Ok(Response::new(Body::empty())) +//! # } +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # let service_that_requires_auth = tower_http::auth::RequireAuthorization::basic( +//! # tower::service_fn(handle), +//! # "username", +//! # "password", +//! # ); +//! let mut client = ServiceBuilder::new() +//! // Use basic auth with the given username and password +//! .layer(AddAuthorizationLayer::basic("username", "password")) +//! .service(service_that_requires_auth); +//! +//! // Make a request, we don't have to add the `Authorization` header manually +//! let response = client +//! .ready() +//! .await? +//! .call(Request::new(Body::empty())) +//! .await?; +//! +//! assert_eq!(StatusCode::OK, response.status()); +//! # Ok(()) +//! # } +//! ``` + +use http::{HeaderValue, Request}; +use std::{ + convert::TryFrom, + task::{Context, Poll}, +}; +use tower_layer::Layer; +use tower_service::Service; + +/// Layer that applies [`AddAuthorization`] which adds authorization to all requests using the +/// [`Authorization`] header. +/// +/// See the [module docs](crate::auth::add_authorization) for an example. +/// +/// You can also use [`SetRequestHeader`] if you have a use case that isn't supported by this +/// middleware. +/// +/// [`Authorization`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization +/// [`SetRequestHeader`]: crate::set_header::SetRequestHeader +#[derive(Debug, Clone)] +pub struct AddAuthorizationLayer { + value: HeaderValue, +} + +impl AddAuthorizationLayer { + /// Authorize requests using a username and password pair. + /// + /// The `Authorization` header will be set to `Basic {credentials}` where `credentials` is + /// `base64_encode("{username}:{password}")`. + /// + /// Since the username and password is sent in clear text it is recommended to use HTTPS/TLS + /// with this method. However use of HTTPS/TLS is not enforced by this middleware. + pub fn basic(username: &str, password: &str) -> Self { + let encoded = base64::encode(format!("{}:{}", username, password)); + let value = HeaderValue::try_from(format!("Basic {}", encoded)).unwrap(); + Self { value } + } + + /// Authorize requests using a "bearer token". Commonly used for OAuth 2. + /// + /// The `Authorization` header will be set to `Bearer {token}`. + /// + /// # Panics + /// + /// Panics if the token is not a valid [`HeaderValue`](http::header::HeaderValue). + pub fn bearer(token: &str) -> Self { + let value = + HeaderValue::try_from(format!("Bearer {}", token)).expect("token is not valid header"); + Self { value } + } + + /// Mark the header as [sensitive]. + /// + /// This can for example be used to hide the header value from logs. + /// + /// [sensitive]: https://docs.rs/http/latest/http/header/struct.HeaderValue.html#method.set_sensitive + #[allow(clippy::wrong_self_convention)] + pub fn as_sensitive(mut self, sensitive: bool) -> Self { + self.value.set_sensitive(sensitive); + self + } +} + +impl Layer for AddAuthorizationLayer { + type Service = AddAuthorization; + + fn layer(&self, inner: S) -> Self::Service { + AddAuthorization { + inner, + value: self.value.clone(), + } + } +} + +/// Middleware that adds authorization all requests using the [`Authorization`] header. +/// +/// See the [module docs](crate::auth::add_authorization) for an example. +/// +/// You can also use [`SetRequestHeader`] if you have a use case that isn't supported by this +/// middleware. +/// +/// [`Authorization`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization +/// [`SetRequestHeader`]: crate::set_header::SetRequestHeader +#[derive(Debug, Clone)] +pub struct AddAuthorization { + inner: S, + value: HeaderValue, +} + +impl AddAuthorization { + /// Authorize requests using a username and password pair. + /// + /// The `Authorization` header will be set to `Basic {credentials}` where `credentials` is + /// `base64_encode("{username}:{password}")`. + /// + /// Since the username and password is sent in clear text it is recommended to use HTTPS/TLS + /// with this method. However use of HTTPS/TLS is not enforced by this middleware. + pub fn basic(inner: S, username: &str, password: &str) -> Self { + AddAuthorizationLayer::basic(username, password).layer(inner) + } + + /// Authorize requests using a "bearer token". Commonly used for OAuth 2. + /// + /// The `Authorization` header will be set to `Bearer {token}`. + /// + /// # Panics + /// + /// Panics if the token is not a valid [`HeaderValue`](http::header::HeaderValue). + pub fn bearer(inner: S, token: &str) -> Self { + AddAuthorizationLayer::bearer(token).layer(inner) + } + + define_inner_service_accessors!(); + + /// Mark the header as [sensitive]. + /// + /// This can for example be used to hide the header value from logs. + /// + /// [sensitive]: https://docs.rs/http/latest/http/header/struct.HeaderValue.html#method.set_sensitive + #[allow(clippy::wrong_self_convention)] + pub fn as_sensitive(mut self, sensitive: bool) -> Self { + self.value.set_sensitive(sensitive); + self + } +} + +impl Service> for AddAuthorization +where + S: Service>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + req.headers_mut() + .insert(http::header::AUTHORIZATION, self.value.clone()); + self.inner.call(req) + } +} + +#[cfg(test)] +mod tests { + #[allow(unused_imports)] + use super::*; + use crate::auth::RequireAuthorizationLayer; + use http::{Response, StatusCode}; + use hyper::Body; + use tower::{BoxError, Service, ServiceBuilder, ServiceExt}; + + #[tokio::test] + async fn basic() { + // service that requires auth for all requests + let svc = ServiceBuilder::new() + .layer(RequireAuthorizationLayer::basic("foo", "bar")) + .service_fn(echo); + + // make a client that adds auth + let mut client = AddAuthorization::basic(svc, "foo", "bar"); + + let res = client + .ready() + .await + .unwrap() + .call(Request::new(Body::empty())) + .await + .unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + } + + #[tokio::test] + async fn token() { + // service that requires auth for all requests + let svc = ServiceBuilder::new() + .layer(RequireAuthorizationLayer::bearer("foo")) + .service_fn(echo); + + // make a client that adds auth + let mut client = AddAuthorization::bearer(svc, "foo"); + + let res = client + .ready() + .await + .unwrap() + .call(Request::new(Body::empty())) + .await + .unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + } + + #[tokio::test] + async fn making_header_sensitive() { + let svc = ServiceBuilder::new() + .layer(RequireAuthorizationLayer::bearer("foo")) + .service_fn(|request: Request| async move { + let auth = request.headers().get(http::header::AUTHORIZATION).unwrap(); + assert!(auth.is_sensitive()); + + Ok::<_, hyper::Error>(Response::new(Body::empty())) + }); + + let mut client = AddAuthorization::bearer(svc, "foo").as_sensitive(true); + + let res = client + .ready() + .await + .unwrap() + .call(Request::new(Body::empty())) + .await + .unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + } + + async fn echo(req: Request) -> Result, BoxError> { + Ok(Response::new(req.into_body())) + } +} diff --git a/tower-http/src/auth/mod.rs b/tower-http/src/auth/mod.rs index 41d67732..da40b149 100644 --- a/tower-http/src/auth/mod.rs +++ b/tower-http/src/auth/mod.rs @@ -1,8 +1,10 @@ //! Authorization related middleware. +pub mod add_authorization; pub mod require_authorization; #[doc(inline)] -pub use self::require_authorization::{ - AuthorizeRequest, RequireAuthorization, RequireAuthorizationLayer, +pub use self::{ + add_authorization::{AddAuthorization, AddAuthorizationLayer}, + require_authorization::{AuthorizeRequest, RequireAuthorization, RequireAuthorizationLayer}, }; diff --git a/tower-http/src/auth/require_authorization.rs b/tower-http/src/auth/require_authorization.rs index 2bf3bcc0..466d65ea 100644 --- a/tower-http/src/auth/require_authorization.rs +++ b/tower-http/src/auth/require_authorization.rs @@ -28,7 +28,7 @@ //! .unwrap(); //! //! let response = service -//! .ready_and() +//! .ready() //! .await? //! .call(request) //! .await?; @@ -41,7 +41,7 @@ //! .unwrap(); //! //! let response = service -//! .ready_and() +//! .ready() //! .await? //! .call(request) //! .await?;