From cac6894ddd20e4b58a586581e4c692c7c0f80c2d Mon Sep 17 00:00:00 2001 From: svartalf Date: Fri, 17 May 2019 17:28:57 +0300 Subject: [PATCH] Experimental middleware for HTTP auth -- alpha.2 version --- Cargo.toml | 8 +- examples/middleware_basic.rs | 89 ------ rustfmt.toml | 4 +- src/extractors/basic.rs | 63 ++-- src/extractors/bearer.rs | 70 +++-- src/extractors/config.rs | 12 +- src/extractors/mod.rs | 25 ++ src/headers/authorization/header.rs | 13 +- src/headers/authorization/scheme/basic.rs | 23 +- src/headers/authorization/scheme/bearer.rs | 12 +- src/headers/authorization/scheme/mod.rs | 7 +- .../www_authenticate/challenge/basic.rs | 8 +- .../challenge/bearer/challenge.rs | 17 +- .../challenge/bearer/errors.rs | 13 +- .../challenge/bearer/tests.rs | 4 +- src/headers/www_authenticate/challenge/mod.rs | 4 +- src/headers/www_authenticate/header.rs | 8 +- src/lib.rs | 13 +- src/middleware.rs | 297 ++++++++++++++++++ src/utils.rs | 3 +- 20 files changed, 497 insertions(+), 196 deletions(-) delete mode 100644 examples/middleware_basic.rs create mode 100644 src/middleware.rs diff --git a/Cargo.toml b/Cargo.toml index 782e5cb38..68b78922e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.3.0-alpha.1" +version = "0.3.0-alpha.2" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -15,13 +15,11 @@ edition = "2018" [dependencies] actix-web = { version = "1.0.0-beta.5", default_features = false } +futures = "0.1" +actix-service = "0.4.0" bytes = "0.4" base64 = "0.10" -[dev-dependencies] -futures = "0.1.27" -actix-service = "0.4.0" - [features] default = [] nightly = [] diff --git a/examples/middleware_basic.rs b/examples/middleware_basic.rs deleted file mode 100644 index a71859ed5..000000000 --- a/examples/middleware_basic.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::borrow::Cow; -use std::io; - -use actix_service::{Service, Transform}; -use actix_web::{dev, web, App, Error, HttpRequest, HttpServer}; -use futures::future::{self, Either, FutureResult}; -use futures::Poll; - -use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; -use actix_web_httpauth::extractors::AuthenticationError; - -struct Auth(Config); - -impl Transform for Auth -where - S: Service, Error = Error>, - S::Future: 'static, -{ - type Request = dev::ServiceRequest; - type Response = dev::ServiceResponse; - type Error = Error; - type Transform = AuthMiddleware; - type InitError = (); - type Future = FutureResult; - - fn new_transform(&self, service: S) -> Self::Future { - future::ok(AuthMiddleware { - service, - auth: self.0.clone(), - }) - } -} - -struct AuthMiddleware { - service: S, - auth: Config, -} - -impl AuthMiddleware { - fn valid_user(credentials: &BasicAuth) -> bool { - let user_id = credentials.user_id(); - let password = credentials.password(); - - user_id == "Alladin" && password == Some(&Cow::Borrowed("open sesame")) - } -} - -impl Service for AuthMiddleware -where - S: Service, Error = Error>, - S::Future: 'static, -{ - type Request = dev::ServiceRequest; - type Response = dev::ServiceResponse; - type Error = Error; - type Future = Either>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.service.poll_ready() - } - - fn call(&mut self, mut req: dev::ServiceRequest) -> Self::Future { - let auth = BasicAuth::from_service_request(&mut req, &self.auth); - - match auth { - Ok(ref credentials) if Self::valid_user(credentials) => Either::A(self.service.call(req)), - Ok(..) => { - let challenge = self.auth.as_ref().clone(); - let error = AuthenticationError::new(challenge); - Either::B(future::err(Self::Error::from(error))) - } - Err(e) => Either::B(future::err(e.into())), - } - } -} - -fn index(_req: HttpRequest) -> String { - "Hello, authorized user!".to_string() -} - -fn main() -> io::Result<()> { - HttpServer::new(|| { - let config = Config::default().realm("WallyWorld"); - - App::new().wrap(Auth(config)).service(web::resource("/").to(index)) - }) - .bind("127.0.0.1:8088")? - .run() -} diff --git a/rustfmt.toml b/rustfmt.toml index 599ac809c..f944c9712 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -2,8 +2,8 @@ unstable_features = true edition = "2018" version = "Two" wrap_comments = true -comment_width = 120 -max_width = 120 +comment_width = 80 +max_width = 80 merge_imports = false newline_style = "Unix" struct_lit_single_line = false diff --git a/src/extractors/basic.rs b/src/extractors/basic.rs index 98f62e398..3ad5deb1b 100644 --- a/src/extractors/basic.rs +++ b/src/extractors/basic.rs @@ -6,8 +6,9 @@ use actix_web::dev::{Payload, ServiceRequest}; use actix_web::http::header::Header; use actix_web::{FromRequest, HttpRequest}; -use super::config::ExtractorConfig; +use super::config::AuthExtractorConfig; use super::errors::AuthenticationError; +use super::AuthExtractor; use crate::headers::authorization::{Authorization, Basic}; use crate::headers::www_authenticate::basic::Basic as Challenge; @@ -15,15 +16,16 @@ use crate::headers::www_authenticate::basic::Basic as Challenge; /// used for [`WWW-Authenticate`] header later. /// /// [`BasicAuth`]: ./struct.BasicAuth.html -/// [`WWW-Authenticate`]: ../../headers/www_authenticate/struct.WwwAuthenticate.html +/// [`WWW-Authenticate`]: +/// ../../headers/www_authenticate/struct.WwwAuthenticate.html #[derive(Debug, Clone, Default)] pub struct Config(Challenge); impl Config { /// Set challenge `realm` attribute. /// - /// The "realm" attribute indicates the scope of protection in the manner described in HTTP/1.1 - /// [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2). + /// The "realm" attribute indicates the scope of protection in the manner + /// described in HTTP/1.1 [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2). pub fn realm(mut self, value: T) -> Config where T: Into>, @@ -39,7 +41,7 @@ impl AsRef for Config { } } -impl ExtractorConfig for Config { +impl AuthExtractorConfig for Config { type Inner = Challenge; fn into_inner(self) -> Self::Inner { @@ -61,7 +63,8 @@ impl ExtractorConfig for Config { /// ``` /// /// If authentication fails, this extractor fetches the [`Config`] instance -/// from the [app data] in order to properly form the `WWW-Authenticate` response header. +/// from the [app data] in order to properly form the `WWW-Authenticate` +/// response header. /// /// ## Example /// @@ -95,27 +98,6 @@ impl BasicAuth { pub fn password(&self) -> Option<&Cow<'static, str>> { self.0.password() } - - /// Try to parse actix-web' `ServiceRequest` and fetch the `BasicAuth` from it. - /// - /// ## Warning - /// - /// This function is used right now for middleware creation only - /// and might change or be totally removed, - /// depends on `actix-web = "1.0"` release changes. - /// - /// This issue will be resolved in the `0.3.0` release. - /// Before that -- brace yourselves! - pub fn from_service_request(req: &mut ServiceRequest, config: &Config) -> ::Future { - Authorization::::parse(&req) - .map(|auth| BasicAuth(auth.into_scheme())) - .map_err(|_| { - // TODO: debug! the original error - let challenge = config.clone().into_inner(); - - AuthenticationError::new(challenge) - }) - } } impl FromRequest for BasicAuth { @@ -123,13 +105,36 @@ impl FromRequest for BasicAuth { type Config = Config; type Error = AuthenticationError; - fn from_request(req: &HttpRequest, _: &mut Payload) -> ::Future { + fn from_request( + req: &HttpRequest, + _: &mut Payload, + ) -> ::Future { Authorization::::parse(req) .map(|auth| BasicAuth(auth.into_scheme())) .map_err(|_| { // TODO: debug! the original error let challenge = req - .get_app_data::() + .app_data::() + .map(|config| config.0.clone()) + // TODO: Add trace! about `Default::default` call + .unwrap_or_else(Default::default); + + AuthenticationError::new(challenge) + }) + } +} + +impl AuthExtractor for BasicAuth { + type Error = AuthenticationError; + type Future = Result; + + fn from_service_request(req: &ServiceRequest) -> Self::Future { + Authorization::::parse(req) + .map(|auth| BasicAuth(auth.into_scheme())) + .map_err(|_| { + // TODO: debug! the original error + let challenge = req + .app_data::() .map(|config| config.0.clone()) // TODO: Add trace! about `Default::default` call .unwrap_or_else(Default::default); diff --git a/src/extractors/bearer.rs b/src/extractors/bearer.rs index 0b88e63a0..a735954c8 100644 --- a/src/extractors/bearer.rs +++ b/src/extractors/bearer.rs @@ -7,8 +7,9 @@ use actix_web::dev::{Payload, ServiceRequest}; use actix_web::http::header::Header; use actix_web::{FromRequest, HttpRequest}; -use super::config::ExtractorConfig; +use super::config::AuthExtractorConfig; use super::errors::AuthenticationError; +use super::AuthExtractor; use crate::headers::authorization; use crate::headers::www_authenticate::bearer; pub use crate::headers::www_authenticate::bearer::Error; @@ -20,8 +21,9 @@ pub struct Config(bearer::Bearer); impl Config { /// Set challenge `scope` attribute. /// - /// The `"scope"` attribute is a space-delimited list of case-sensitive scope values - /// indicating the required scope of the access token for accessing the requested resource. + /// The `"scope"` attribute is a space-delimited list of case-sensitive + /// scope values indicating the required scope of the access token for + /// accessing the requested resource. pub fn scope>>(mut self, value: T) -> Config { self.0.scope = Some(value.into()); self @@ -29,8 +31,8 @@ impl Config { /// Set challenge `realm` attribute. /// - /// The "realm" attribute indicates the scope of protection in the manner described in HTTP/1.1 - /// [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2). + /// The "realm" attribute indicates the scope of protection in the manner + /// described in HTTP/1.1 [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2). pub fn realm>>(mut self, value: T) -> Config { self.0.realm = Some(value.into()); self @@ -43,7 +45,7 @@ impl AsRef for Config { } } -impl ExtractorConfig for Config { +impl AuthExtractorConfig for Config { type Inner = bearer::Bearer; fn into_inner(self) -> Self::Inner { @@ -64,7 +66,8 @@ impl ExtractorConfig for Config { /// ``` /// /// If authentication fails, this extractor fetches the [`Config`] instance -/// from the [app data] in order to properly form the `WWW-Authenticate` response header. +/// from the [app data] in order to properly form the `WWW-Authenticate` +/// response header. /// /// ## Example /// @@ -78,7 +81,11 @@ impl ExtractorConfig for Config { /// /// fn main() { /// let app = App::new() -/// .data(Config::default().realm("Restricted area").scope("email photo")) +/// .data( +/// Config::default() +/// .realm("Restricted area") +/// .scope("email photo"), +/// ) /// .service(web::resource("/index.html").route(web::get().to(index))); /// } /// ``` @@ -90,27 +97,6 @@ impl BearerAuth { pub fn token(&self) -> &str { self.0.token() } - - /// Try to parse actix-web' `ServiceRequest` and fetch the `BasicAuth` from it. - /// - /// ## Warning - /// - /// This function is used right now for middleware creation only - /// and might change or be totally removed, - /// depends on `actix-web = "1.0"` release changes. - /// - /// This issue will be resolved in the `0.3.0` release. - /// Before that -- brace yourselves! - pub fn from_service_request(req: &mut ServiceRequest, config: &Config) -> ::Future { - authorization::Authorization::::parse(&req) - .map(|auth| BearerAuth(auth.into_scheme())) - .map_err(|_| { - // TODO: debug! the original error - let challenge = config.clone().into_inner(); - - AuthenticationError::new(challenge) - }) - } } impl FromRequest for BearerAuth { @@ -118,7 +104,10 @@ impl FromRequest for BearerAuth { type Future = Result; type Error = AuthenticationError; - fn from_request(req: &HttpRequest, _payload: &mut Payload) -> ::Future { + fn from_request( + req: &HttpRequest, + _payload: &mut Payload, + ) -> ::Future { authorization::Authorization::::parse(req) .map(|auth| BearerAuth(auth.into_scheme())) .map_err(|_| { @@ -132,11 +121,30 @@ impl FromRequest for BearerAuth { } } +impl AuthExtractor for BearerAuth { + type Future = Result; + type Error = AuthenticationError; + + fn from_service_request(req: &ServiceRequest) -> Self::Future { + authorization::Authorization::::parse(req) + .map(|auth| BearerAuth(auth.into_scheme())) + .map_err(|_| { + let bearer = req + .app_data::() + .map(|config| config.0.clone()) + .unwrap_or_else(Default::default); + + AuthenticationError::new(bearer) + }) + } +} + /// Extended error customization for HTTP `Bearer` auth. impl AuthenticationError { /// Attach `Error` to the current Authentication error. /// - /// Error status code will be changed to the one provided by the `kind` Error. + /// Error status code will be changed to the one provided by the `kind` + /// Error. pub fn with_error(mut self, kind: Error) -> Self { *self.status_code_mut() = kind.status_code(); self.challenge_mut().error = Some(kind); diff --git a/src/extractors/config.rs b/src/extractors/config.rs index 721cf8f5b..2faf869d9 100644 --- a/src/extractors/config.rs +++ b/src/extractors/config.rs @@ -1,15 +1,21 @@ use super::AuthenticationError; use crate::headers::www_authenticate::Challenge; -pub trait ExtractorConfig { +/// Trait implemented for types that provides configuration +/// for the authentication [extractors]. +/// +/// [extractors]: ./trait.AuthExtractor.html +pub trait AuthExtractorConfig { + /// Associated challenge type. type Inner: Challenge; + /// Convert the config instance into a HTTP challenge. fn into_inner(self) -> Self::Inner; } -impl From for AuthenticationError<::Inner> +impl From for AuthenticationError<::Inner> where - T: ExtractorConfig, + T: AuthExtractorConfig, { fn from(config: T) -> Self { AuthenticationError::new(config.into_inner()) diff --git a/src/extractors/mod.rs b/src/extractors/mod.rs index 797c99713..062efba2d 100644 --- a/src/extractors/mod.rs +++ b/src/extractors/mod.rs @@ -1,8 +1,33 @@ //! Type-safe authentication information extractors +use actix_web::dev::ServiceRequest; +use actix_web::Error; +use futures::IntoFuture; + pub mod basic; pub mod bearer; mod config; mod errors; +pub use self::config::AuthExtractorConfig; pub use self::errors::AuthenticationError; + +/// Trait implemented by types that can extract +/// HTTP authentication scheme credentials from the request. +/// +/// It is very similar to actix' `FromRequest` trait, +/// except it operates with a `ServiceRequest` struct instead, +/// therefore it can be used in the middlewares. +/// +/// You will not need it unless you want to implement your own +/// authentication scheme. +pub trait AuthExtractor: Sized { + /// The associated error which can be returned. + type Error: Into; + + /// Future that resolves into extracted credentials type. + type Future: IntoFuture; + + /// Parse the authentication credentials from the actix' `ServiceRequest`. + fn from_service_request(req: &ServiceRequest) -> Self::Future; +} diff --git a/src/headers/authorization/header.rs b/src/headers/authorization/header.rs index 740af59b5..d6f051124 100644 --- a/src/headers/authorization/header.rs +++ b/src/headers/authorization/header.rs @@ -1,7 +1,9 @@ use std::fmt; use actix_web::error::ParseError; -use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION}; +use actix_web::http::header::{ + Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION, +}; use actix_web::HttpMessage; use crate::headers::authorization::scheme::Scheme; @@ -14,7 +16,8 @@ use crate::headers::authorization::scheme::Scheme; /// credentials containing the authentication information of the user /// agent for the realm of the resource being requested. /// -/// `Authorization` header is generic over [authentication scheme](./trait.Scheme.html). +/// `Authorization` header is generic over [authentication +/// scheme](./trait.Scheme.html). /// /// # Example /// @@ -35,7 +38,8 @@ impl Authorization where S: Scheme, { - /// Consumes `Authorization` header and returns inner [`Scheme`] implementation. + /// Consumes `Authorization` header and returns inner [`Scheme`] + /// implementation. /// /// [`Scheme`]: ./trait.Scheme.html pub fn into_scheme(self) -> S { @@ -77,7 +81,8 @@ impl Header for Authorization { } fn parse(msg: &T) -> Result { - let header = msg.headers().get(AUTHORIZATION).ok_or(ParseError::Header)?; + let header = + msg.headers().get(AUTHORIZATION).ok_or(ParseError::Header)?; let scheme = S::parse(header).map_err(|_| ParseError::Header)?; Ok(Authorization(scheme)) diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 06e8aabc2..6c91d6009 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use std::fmt; use std::str; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use actix_web::http::header::{ + HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, +}; use base64; use bytes::{BufMut, BytesMut}; @@ -18,7 +20,8 @@ pub struct Basic { } impl Basic { - /// Creates `Basic` credentials with provided `user_id` and optional `password`. + /// Creates `Basic` credentials with provided `user_id` and optional + /// `password`. /// /// ## Example /// @@ -102,15 +105,19 @@ impl IntoHeaderValue for Basic { type Error = InvalidHeaderValueBytes; fn try_into(self) -> Result::Error> { - let mut credentials = - BytesMut::with_capacity(self.user_id.len() + 1 + self.password.as_ref().map_or(0, |pwd| pwd.len())); + let mut credentials = BytesMut::with_capacity( + self.user_id.len() + + 1 + + self.password.as_ref().map_or(0, |pwd| pwd.len()), + ); utils::put_cow(&mut credentials, &self.user_id); credentials.put_u8(b':'); if let Some(ref password) = self.password { utils::put_cow(&mut credentials, password); } - // TODO: It would be nice not to allocate new `String` here but write directly to `value` + // TODO: It would be nice not to allocate new `String` here but write + // directly to `value` let encoded = base64::encode(&credentials); let mut value = BytesMut::with_capacity(6 + encoded.len()); value.put("Basic "); @@ -127,7 +134,8 @@ mod tests { #[test] fn test_parse_header() { - let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + let value = + HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); let scheme = Basic::parse(&value); assert!(scheme.is_ok()); @@ -205,7 +213,8 @@ mod benches { #[bench] fn bench_parsing(b: &mut Bencher) { - let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + let value = + HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); b.iter(|| Basic::parse(&value)); } diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs index c350c1deb..78b532a58 100644 --- a/src/headers/authorization/scheme/bearer.rs +++ b/src/headers/authorization/scheme/bearer.rs @@ -1,7 +1,9 @@ use std::borrow::Cow; use std::fmt; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use actix_web::http::header::{ + HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, +}; use bytes::{BufMut, BytesMut}; use crate::headers::authorization::errors::ParseError; @@ -10,7 +12,8 @@ use crate::utils; /// Credentials for `Bearer` authentication scheme, defined in [RFC6750](https://tools.ietf.org/html/rfc6750) /// -/// Should be used in combination with [`Authorization`](./struct.Authorization.html) header. +/// Should be used in combination with +/// [`Authorization`](./struct.Authorization.html) header. #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] pub struct Bearer { token: Cow<'static, str>, @@ -130,6 +133,9 @@ mod tests { let result = bearer.try_into(); assert!(result.is_ok()); - assert_eq!(result.unwrap(), HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM")); + assert_eq!( + result.unwrap(), + HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM") + ); } } diff --git a/src/headers/authorization/scheme/mod.rs b/src/headers/authorization/scheme/mod.rs index 5832965f3..1ab534c93 100644 --- a/src/headers/authorization/scheme/mod.rs +++ b/src/headers/authorization/scheme/mod.rs @@ -7,8 +7,11 @@ pub mod bearer; use crate::headers::authorization::errors::ParseError; -/// Authentication scheme for [`Authorization`](./struct.Authorization.html) header. -pub trait Scheme: IntoHeaderValue + Debug + Display + Clone + Send + Sync { +/// Authentication scheme for [`Authorization`](./struct.Authorization.html) +/// header. +pub trait Scheme: + IntoHeaderValue + Debug + Display + Clone + Send + Sync +{ /// Try to parse the authentication scheme from the `Authorization` header. fn parse(header: &HeaderValue) -> Result; } diff --git a/src/headers/www_authenticate/challenge/basic.rs b/src/headers/www_authenticate/challenge/basic.rs index c3c4f22cb..86c637c98 100644 --- a/src/headers/www_authenticate/challenge/basic.rs +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -5,7 +5,9 @@ use std::default::Default; use std::fmt; use std::str; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use actix_web::http::header::{ + HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, +}; use bytes::{BufMut, Bytes, BytesMut}; use super::Challenge; @@ -24,7 +26,9 @@ use crate::utils; /// fn index(_req: HttpRequest) -> HttpResponse { /// let challenge = Basic::with_realm("Restricted area"); /// -/// HttpResponse::Unauthorized().set(WwwAuthenticate(challenge)).finish() +/// HttpResponse::Unauthorized() +/// .set(WwwAuthenticate(challenge)) +/// .finish() /// } /// ``` /// diff --git a/src/headers/www_authenticate/challenge/bearer/challenge.rs b/src/headers/www_authenticate/challenge/bearer/challenge.rs index 8a03372f3..025b0e585 100644 --- a/src/headers/www_authenticate/challenge/bearer/challenge.rs +++ b/src/headers/www_authenticate/challenge/bearer/challenge.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use std::fmt; use std::str; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use actix_web::http::header::{ + HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, +}; use bytes::{BufMut, Bytes, BytesMut}; use super::super::Challenge; @@ -16,7 +18,9 @@ use crate::utils; /// /// ```rust /// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; -/// use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer, Error}; +/// use actix_web_httpauth::headers::www_authenticate::bearer::{ +/// Bearer, Error, +/// }; /// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate; /// /// fn index(_req: HttpRequest) -> HttpResponse { @@ -28,7 +32,9 @@ use crate::utils; /// .error_uri("http://example.org") /// .finish(); /// -/// HttpResponse::Unauthorized().set(WwwAuthenticate(challenge)).finish() +/// HttpResponse::Unauthorized() +/// .set(WwwAuthenticate(challenge)) +/// .finish() /// } /// ``` /// @@ -62,7 +68,10 @@ impl Bearer { #[doc(hidden)] impl Challenge for Bearer { fn to_bytes(&self) -> Bytes { - let desc_uri_required = self.error_description.as_ref().map_or(0, |desc| desc.len() + 20) + let desc_uri_required = self + .error_description + .as_ref() + .map_or(0, |desc| desc.len() + 20) + self.error_uri.as_ref().map_or(0, |url| url.len() + 12); let capacity = 6 + self.realm.as_ref().map_or(0, |realm| realm.len() + 9) diff --git a/src/headers/www_authenticate/challenge/bearer/errors.rs b/src/headers/www_authenticate/challenge/bearer/errors.rs index 5509fccec..7bea8d4a4 100644 --- a/src/headers/www_authenticate/challenge/bearer/errors.rs +++ b/src/headers/www_authenticate/challenge/bearer/errors.rs @@ -5,15 +5,18 @@ use actix_web::http::StatusCode; /// Bearer authorization error types, described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3.1) #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum Error { - /// The request is missing a required parameter, includes an unsupported parameter - /// or parameter value, repeats the same parameter, uses more than one method - /// for including an access token, or is otherwise malformed. + /// The request is missing a required parameter, includes an unsupported + /// parameter or parameter value, repeats the same parameter, uses more + /// than one method for including an access token, or is otherwise + /// malformed. InvalidRequest, - /// The access token provided is expired, revoked, malformed, or invalid for other reasons. + /// The access token provided is expired, revoked, malformed, or invalid + /// for other reasons. InvalidToken, - /// The request requires higher privileges than provided by the access token. + /// The request requires higher privileges than provided by the access + /// token. InsufficientScope, } diff --git a/src/headers/www_authenticate/challenge/bearer/tests.rs b/src/headers/www_authenticate/challenge/bearer/tests.rs index 03f088e17..015c07d33 100644 --- a/src/headers/www_authenticate/challenge/bearer/tests.rs +++ b/src/headers/www_authenticate/challenge/bearer/tests.rs @@ -4,7 +4,9 @@ use super::*; fn to_bytes() { let b = Bearer::build() .error(Error::InvalidToken) - .error_description("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found") + .error_description( + "Subject 8740827c-2e0a-447b-9716-d73042e4039d not found", + ) .finish(); assert_eq!( diff --git a/src/headers/www_authenticate/challenge/mod.rs b/src/headers/www_authenticate/challenge/mod.rs index ce70f4fe2..619c5ef51 100644 --- a/src/headers/www_authenticate/challenge/mod.rs +++ b/src/headers/www_authenticate/challenge/mod.rs @@ -7,7 +7,9 @@ pub mod basic; pub mod bearer; /// Authentication challenge for `WWW-Authenticate` header. -pub trait Challenge: IntoHeaderValue + Debug + Display + Clone + Send + Sync { +pub trait Challenge: + IntoHeaderValue + Debug + Display + Clone + Send + Sync +{ /// Converts the challenge into a bytes suitable for HTTP transmission. fn to_bytes(&self) -> Bytes; } diff --git a/src/headers/www_authenticate/header.rs b/src/headers/www_authenticate/header.rs index 6f08c935f..092b7593b 100644 --- a/src/headers/www_authenticate/header.rs +++ b/src/headers/www_authenticate/header.rs @@ -1,5 +1,7 @@ use actix_web::error::ParseError; -use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE}; +use actix_web::http::header::{ + Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE, +}; use actix_web::HttpMessage; use super::Challenge; @@ -7,8 +9,8 @@ use super::Challenge; /// `WWW-Authenticate` header, described in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.1) /// /// This header is generic over [Challenge](./trait.Challenge.html) trait, -/// see [Basic](./basic/struct.Basic.html) and [Bearer](./bearer/struct.Bearer.html) -/// challenges for details. +/// see [Basic](./basic/struct.Basic.html) and +/// [Bearer](./bearer/struct.Bearer.html) challenges for details. #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)] pub struct WwwAuthenticate(pub C); diff --git a/src/lib.rs b/src/lib.rs index 8ada78c6a..422eace13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ //! HTTP Authorization support for [actix-web](https://actix.rs) framework. //! -//! Provides [Authorization] and [WWW-Authenticate] headers, -//! and [extractors] for an [Authorization] header. +//! Provides: +//! * typed [Authorization] and [WWW-Authenticate] headers +//! * [extractors] for an [Authorization] header +//! * [middleware] for easier authorization checking //! //! ## Supported schemes //! @@ -11,11 +13,14 @@ //! [Authorization]: `crate::headers::authorization::Authorization` //! [WWW-Authenticate]: `crate::headers::www_authenticate::WwwAuthenticate` //! [extractors]: https://actix.rs/docs/extractors/ +//! [middleware]: ./middleware/ -#![forbid(bare_trait_objects)] -#![forbid(missing_docs)] +#![deny(bare_trait_objects)] +#![deny(missing_docs)] +#![deny(unused)] #![cfg_attr(feature = "nightly", feature(test))] pub mod extractors; pub mod headers; +pub mod middleware; mod utils; diff --git a/src/middleware.rs b/src/middleware.rs new file mode 100644 index 000000000..c2eceb12d --- /dev/null +++ b/src/middleware.rs @@ -0,0 +1,297 @@ +//! HTTP Authentication middleware. + +use std::marker::PhantomData; +use std::rc::Rc; + +use actix_service::{Service, Transform}; +use actix_web::dev::{ServiceRequest, ServiceResponse}; +use actix_web::Error; +use futures::future::{self, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll}; + +use crate::extractors::{basic, bearer, AuthExtractor}; + +/// Middleware for checking HTTP authentication. +/// +/// If there is no `Authorization` header in the request, +/// this middleware returns an error immediately, +/// without calling the `F` callback. +/// Otherwise, it will pass parsed credentials into it. +pub struct HttpAuthentication +where + T: AuthExtractor, +{ + validator_fn: Rc, + _extractor: PhantomData, +} + +impl HttpAuthentication +where + T: AuthExtractor, + F: FnMut(&mut ServiceRequest, T) -> O, + O: IntoFuture, +{ + /// Construct `HttpAuthentication` middleware + /// with the provided auth extractor `T` and + /// validation callback `F`. + pub fn with_fn(validator_fn: F) -> HttpAuthentication { + HttpAuthentication { + validator_fn: Rc::new(validator_fn), + _extractor: PhantomData, + } + } +} + +impl HttpAuthentication +where + F: FnMut(&mut ServiceRequest, basic::BasicAuth) -> O, + O: IntoFuture, +{ + /// Construct `HttpAuthentication` middleware for the HTTP "Basic" + /// authentication scheme. + /// + /// ## Example + /// + /// ``` + /// # use actix_web::Error; + /// # use actix_web::dev::ServiceRequest; + /// # use futures::future::{self, FutureResult}; + /// # use actix_web_httpauth::middleware::HttpAuthentication; + /// # use actix_web_httpauth::extractors::basic::BasicAuth; + /// // In this example validator returns immediately, + /// // but since it is required to return anything + /// // that implements `IntoFuture` trait, + /// // it can be extended to query database + /// // or to do something else in a async manner. + /// fn validator( + /// req: &mut ServiceRequest, + /// credentials: BasicAuth, + /// ) -> FutureResult<(), Error> { + /// // All users are great and more than welcome! + /// future::ok(()) + /// } + /// + /// let middleware = HttpAuthentication::basic(validator); + /// ``` + pub fn basic(validator_fn: F) -> Self { + Self::with_fn(validator_fn) + } +} + +impl HttpAuthentication +where + F: FnMut(&mut ServiceRequest, bearer::BearerAuth) -> O, + O: IntoFuture, +{ + /// Construct `HttpAuthentication` middleware for the HTTP "Bearer" + /// authentication scheme. + /// ## Example + /// + /// ``` + /// # use actix_web::Error; + /// # use actix_web::dev::ServiceRequest; + /// # use futures::future::{self, FutureResult}; + /// # use actix_web_httpauth::middleware::HttpAuthentication; + /// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth}; + /// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig}; + /// fn validator(req: &mut ServiceRequest, credentials: BearerAuth) -> FutureResult<(), Error> { + /// if credentials.token() == "mF_9.B5f-4.1JqM" { + /// future::ok(()) + /// } else { + /// let config = req.app_data::() + /// .map(|data| data.get_ref().clone()) + /// .unwrap_or_else(Default::default) + /// .scope("urn:example:channel=HBO&urn:example:rating=G,PG-13"); + /// + /// future::err(AuthenticationError::from(config).into()) + /// } + /// } + /// + /// let middleware = HttpAuthentication::bearer(validator); + /// ``` + pub fn bearer(validator_fn: F) -> Self { + Self::with_fn(validator_fn) + } +} + +impl Transform for HttpAuthentication +where + S: Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + S::Future: 'static, + F: Fn(&mut ServiceRequest, T) -> O + 'static, + O: IntoFuture + 'static, + T: AuthExtractor + 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Transform = AuthenticationMiddleware; + type InitError = (); + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + future::ok(AuthenticationMiddleware { + service: Some(service), + validator_fn: self.validator_fn.clone(), + _extractor: PhantomData, + }) + } +} + +#[doc(hidden)] +pub struct AuthenticationMiddleware +where + T: AuthExtractor, +{ + service: Option, + validator_fn: Rc, + _extractor: PhantomData, +} + +impl Service for AuthenticationMiddleware +where + S: Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + S::Future: 'static, + F: Fn(&mut ServiceRequest, T) -> O + 'static, + O: IntoFuture + 'static, + T: AuthExtractor + 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = S::Error; + type Future = Box, Error = Error>>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service + .as_mut() + .expect("AuthenticationMiddleware was called already") + .poll_ready() + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + let validator_fn = self.validator_fn.clone(); + let mut service = self + .service + .take() + .expect("AuthenticationMiddleware was called twice"); + + let f = Extract::new(req) + .and_then(move |(req, credentials)| { + Validate::new(req, validator_fn, credentials) + }) + .and_then(move |req| service.call(req)); + + Box::new(f) + } +} + +struct Extract { + req: Option, + f: Option>>, + _extractor: PhantomData, +} + +impl Extract { + pub fn new(req: ServiceRequest) -> Self { + Extract { + req: Some(req), + f: None, + _extractor: PhantomData, + } + } +} + +impl Future for Extract +where + T: AuthExtractor, + T::Future: 'static, + T::Error: 'static, +{ + type Item = (ServiceRequest, T); + type Error = Error; + + fn poll(&mut self) -> Poll { + if self.f.is_none() { + let req = + self.req.as_ref().expect("Extract future was polled twice!"); + let f = T::from_service_request(req) + .into_future() + .map_err(Into::into); + self.f = Some(Box::new(f)); + } + + let f = self + .f + .as_mut() + .expect("Extraction future should be initialized at this point"); + let credentials = futures::try_ready!(f.poll()); + + let req = self.req.take().expect("Extract future was polled twice!"); + Ok(Async::Ready((req, credentials))) + } +} + +struct Validate { + req: Option, + validation_f: Option>>, + validator_fn: Rc, + credentials: Option, +} + +impl Validate { + pub fn new( + req: ServiceRequest, + validator_fn: Rc, + credentials: T, + ) -> Self { + Validate { + req: Some(req), + credentials: Some(credentials), + validator_fn, + validation_f: None, + } + } +} + +impl Future for Validate +where + F: Fn(&mut ServiceRequest, T) -> O, + O: IntoFuture + 'static, +{ + type Item = ServiceRequest; + type Error = Error; + + fn poll(&mut self) -> Poll { + if self.validation_f.is_none() { + let req = self + .req + .as_mut() + .expect("Unable to get the mutable access to the request"); + let credentials = self + .credentials + .take() + .expect("Validate future was polled in some weird manner"); + let f = (self.validator_fn)(req, credentials).into_future(); + + self.validation_f = Some(Box::new(f)); + } + + let f = self + .validation_f + .as_mut() + .expect("Validation future should exist at this moment"); + // We do not care about returned `Ok(())` + futures::try_ready!(f.poll()); + let req = self.req.take().expect("Validate future was polled already"); + + Ok(Async::Ready(req)) + } +} diff --git a/src/utils.rs b/src/utils.rs index e6b7297e9..e30faedf3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,8 @@ use std::borrow::Cow; use bytes::{BufMut, BytesMut}; -// `bytes::Buf` is not implemented for `Cow<'static, str>`, implementing it by ourselves. +// `bytes::Buf` is not implemented for `Cow<'static, str>`, implementing it by +// ourselves. #[inline] #[allow(clippy::ptr_arg)] // Totally okay to accept the reference to Cow here pub fn put_cow(buf: &mut BytesMut, value: &Cow<'static, str>) {