diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..f6da06d69 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/.travis.yml b/.travis.yml index 45793de3b..d2da893a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,3 @@ rust: - stable - beta - nightly -matrix: - allow_failures: - - rust: nightly diff --git a/CHANGELOG.md b/CHANGELOG.md index 734c338be..82ff6c8cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## Unreleased + - Crate edition was changed to `2018`, same as `actix-web` + - Depends on `actix-web = "^1.0"` version now + - `WWWAuthenticate` header struct was renamed into `WwwAuthenticate` + - Challenges and extractor configs are now operating with `Cow<'static, str>` types instead of `String` types + ## [0.2.0] - 2019-04-26 ### Changed - `actix-web` dependency is used without default features now ([#6](https://github.com/svartalf/actix-web-httpauth/pull/6)) diff --git a/Cargo.toml b/Cargo.toml index d0a388dff..782e5cb38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.2.0" +version = "0.3.0-alpha.1" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -9,14 +9,19 @@ homepage = "https://github.com/svartalf/actix-web-httpauth" repository = "https://github.com/svartalf/actix-web-httpauth.git" documentation = "https://docs.rs/actix-web-httpauth/" categories = ["web-programming::http-server"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" exclude = [".travis.yml", ".gitignore"] +edition = "2018" [dependencies] -actix-web = { version = "0.7", default_features = false } +actix-web = { version = "1.0.0-beta.5", default_features = false } bytes = "0.4" base64 = "0.10" +[dev-dependencies] +futures = "0.1.27" +actix-service = "0.4.0" + [features] default = [] nightly = [] diff --git a/examples/extractor_basic.rs b/examples/extractor_basic.rs deleted file mode 100644 index 043289a04..000000000 --- a/examples/extractor_basic.rs +++ /dev/null @@ -1,36 +0,0 @@ -extern crate actix_web; -extern crate actix_web_httpauth; - -use actix_web::{server, App, Result, HttpRequest, FromRequest}; -use actix_web::middleware::{Middleware, Started}; -use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; -use actix_web_httpauth::extractors::AuthenticationError; - -struct Auth; - -impl Middleware for Auth { - fn start(&self, req: &HttpRequest) -> Result { - let mut config = Config::default(); - config.realm("WallyWorld"); - let auth = BasicAuth::from_request(&req, &config)?; - - if auth.username() == "Aladdin" && auth.password() == Some("open sesame") { - Ok(Started::Done) - } else { - Err(AuthenticationError::from(config).into()) - } - } -} - -fn index(_req: HttpRequest) -> String { - "Hello, authorized user!".to_string() -} - -fn main() { - server::new(|| App::new() - .middleware(Auth) - .resource("/", |r| r.with(index)) - ) - .bind("127.0.0.1:8088").unwrap() - .run(); -} diff --git a/examples/extractor_bearer.rs b/examples/extractor_bearer.rs deleted file mode 100644 index 560eb74d4..000000000 --- a/examples/extractor_bearer.rs +++ /dev/null @@ -1,40 +0,0 @@ -extern crate actix_web; -extern crate actix_web_httpauth; - -use actix_web::{server, App, HttpRequest, Result, FromRequest}; -use actix_web_httpauth::extractors::AuthenticationError; -use actix_web_httpauth::extractors::bearer::{BearerAuth, Config, Error}; -use actix_web::middleware::{Middleware, Started}; - -struct Auth; - -impl Middleware for Auth { - fn start(&self, req: &HttpRequest) -> Result { - let mut config = Config::default(); - config.realm("Restricted area"); - config.scope("openid profile email"); - let auth = BearerAuth::from_request(&req, &config)?; - - if auth.token() == "mF_9.B5f-4.1JqM" { - Ok(Started::Done) - } else { - Err(AuthenticationError::from(config) - .with_error(Error::InvalidToken) - .into()) - } - } - -} - -fn index(_req: HttpRequest) -> String { - "Hello, authorized user!".to_string() -} - -fn main() { - server::new(|| App::new() - .middleware(Auth) - .resource("/", |r| r.with(index)) - ) - .bind("127.0.0.1:8088").unwrap() - .run(); -} diff --git a/examples/header_www_authenticate_basic.rs b/examples/header_www_authenticate_basic.rs deleted file mode 100644 index 031948b20..000000000 --- a/examples/header_www_authenticate_basic.rs +++ /dev/null @@ -1,25 +0,0 @@ -extern crate actix_web; -extern crate actix_web_httpauth; - -use actix_web::{server, App, HttpRequest, HttpResponse}; -use actix_web::http::StatusCode; -use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate}; -use actix_web_httpauth::headers::www_authenticate::basic::Basic; - - -fn index(req: HttpRequest) -> HttpResponse { - let challenge = Basic { - realm: Some("Restricted area".to_string()), - }; - - req.build_response(StatusCode::UNAUTHORIZED) - .set(WWWAuthenticate(challenge)) - .finish() -} - -fn main() { - server::new(|| App::new() - .resource("/", |r| r.with(index))) - .bind("127.0.0.1:8088").unwrap() - .run(); -} diff --git a/examples/header_www_authenticate_bearer.rs b/examples/header_www_authenticate_bearer.rs deleted file mode 100644 index 65d24b822..000000000 --- a/examples/header_www_authenticate_bearer.rs +++ /dev/null @@ -1,29 +0,0 @@ -extern crate actix_web; -extern crate actix_web_httpauth; - -use actix_web::{server, App, HttpRequest, HttpResponse}; -use actix_web::http::StatusCode; -use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate}; -use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer, Error}; - - -fn index(req: HttpRequest) -> HttpResponse { - let challenge = Bearer { - realm: Some("example".to_string()), - scope: Some("openid profile email".to_string()), - error: Some(Error::InvalidToken), - error_description: Some("The access token expired".to_string()), - error_uri: Some("http://example.org".to_string()), - }; - - req.build_response(StatusCode::UNAUTHORIZED) - .set(WWWAuthenticate(challenge)) - .finish() -} - -fn main() { - server::new(|| App::new() - .resource("/", |r| r.with(index))) - .bind("127.0.0.1:8088").unwrap() - .run(); -} diff --git a/examples/middleware_basic.rs b/examples/middleware_basic.rs new file mode 100644 index 000000000..a71859ed5 --- /dev/null +++ b/examples/middleware_basic.rs @@ -0,0 +1,89 @@ +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 new file mode 100644 index 000000000..599ac809c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,9 @@ +unstable_features = true +edition = "2018" +version = "Two" +wrap_comments = true +comment_width = 120 +max_width = 120 +merge_imports = false +newline_style = "Unix" +struct_lit_single_line = false diff --git a/src/extractors/basic.rs b/src/extractors/basic.rs index 484058bb6..98f62e398 100644 --- a/src/extractors/basic.rs +++ b/src/extractors/basic.rs @@ -1,16 +1,22 @@ -use std::default::Default; +//! Extractor for the "Basic" HTTP Authentication Scheme -use actix_web::{HttpRequest, FromRequest}; +use std::borrow::Cow; + +use actix_web::dev::{Payload, ServiceRequest}; use actix_web::http::header::Header; +use actix_web::{FromRequest, HttpRequest}; -use headers::authorization::{Authorization, Basic}; -use headers::www_authenticate::basic::Basic as Challenge; -use super::errors::AuthenticationError; use super::config::ExtractorConfig; +use super::errors::AuthenticationError; +use crate::headers::authorization::{Authorization, Basic}; +use crate::headers::www_authenticate::basic::Basic as Challenge; -/// [`BasicAuth`](./struct.BasicAuth.html) extractor configuration, -/// used for `WWW-Authenticate` header later. -#[derive(Debug, Clone)] +/// [`BasicAuth`] extractor configuration, +/// used for [`WWW-Authenticate`] header later. +/// +/// [`BasicAuth`]: ./struct.BasicAuth.html +/// [`WWW-Authenticate`]: ../../headers/www_authenticate/struct.WwwAuthenticate.html +#[derive(Debug, Clone, Default)] pub struct Config(Challenge); impl Config { @@ -18,12 +24,21 @@ impl Config { /// /// 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) -> &mut Config { + pub fn realm(mut self, value: T) -> Config + where + T: Into>, + { self.0.realm = Some(value.into()); self } } +impl AsRef for Config { + fn as_ref(&self) -> &Challenge { + &self.0 + } +} + impl ExtractorConfig for Config { type Inner = Challenge; @@ -32,49 +47,94 @@ impl ExtractorConfig for Config { } } -impl Default for Config { - fn default() -> Self { - Config(Challenge::default()) - } -} - -/// Extractor for HTTP Basic auth +/// Extractor for HTTP Basic auth. /// /// # Example /// /// ```rust -/// # extern crate actix_web; -/// # extern crate actix_web_httpauth; /// use actix_web::Result; /// use actix_web_httpauth::extractors::basic::BasicAuth; /// -/// fn index(auth: BasicAuth) -> Result { -/// Ok(format!("Hello, {}!", auth.username())) +/// fn index(auth: BasicAuth) -> String { +/// format!("Hello, {}!", auth.user_id()) /// } /// ``` +/// +/// If authentication fails, this extractor fetches the [`Config`] instance +/// from the [app data] in order to properly form the `WWW-Authenticate` response header. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::{web, App}; +/// use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; +/// +/// fn index(auth: BasicAuth) -> String { +/// format!("Hello, {}!", auth.user_id()) +/// } +/// +/// fn main() { +/// let app = App::new() +/// .data(Config::default().realm("Restricted area")) +/// .service(web::resource("/index.html").route(web::get().to(index))); +/// } +/// ``` +/// +/// [`Config`]: ./struct.Config.html +/// [app data]: https://docs.rs/actix-web/1.0.0-beta.5/actix_web/struct.App.html#method.data #[derive(Debug, Clone)] pub struct BasicAuth(Basic); impl BasicAuth { - pub fn username(&self) -> &str { - self.0.username.as_str() + /// Returns client's user-ID. + pub fn user_id(&self) -> &Cow<'static, str> { + &self.0.user_id() } - pub fn password(&self) -> Option<&str> { - match self.0.password { - None => None, - Some(ref pwd) => Some(pwd.as_str()) - } + /// Returns client's password. + 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 { +impl FromRequest for BasicAuth { + type Future = Result; type Config = Config; - type Result = Result>; + type Error = AuthenticationError; - fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { + fn from_request(req: &HttpRequest, _: &mut Payload) -> ::Future { Authorization::::parse(req) - .map(|auth| BasicAuth(auth.into_inner())) - .map_err(|_| AuthenticationError::new(cfg.0.clone())) + .map(|auth| BasicAuth(auth.into_scheme())) + .map_err(|_| { + // TODO: debug! the original error + let challenge = req + .get_app_data::() + .map(|config| config.0.clone()) + // TODO: Add trace! about `Default::default` call + .unwrap_or_else(Default::default); + + AuthenticationError::new(challenge) + }) } } diff --git a/src/extractors/bearer.rs b/src/extractors/bearer.rs index b3c9a5182..0b88e63a0 100644 --- a/src/extractors/bearer.rs +++ b/src/extractors/bearer.rs @@ -1,16 +1,20 @@ +//! Extractor for the "Bearer" HTTP Authentication Scheme + +use std::borrow::Cow; use std::default::Default; -use actix_web::{HttpRequest, FromRequest}; +use actix_web::dev::{Payload, ServiceRequest}; use actix_web::http::header::Header; +use actix_web::{FromRequest, HttpRequest}; -use headers::authorization; -use headers::www_authenticate::bearer; -pub use headers::www_authenticate::bearer::Error; -use super::errors::AuthenticationError; use super::config::ExtractorConfig; +use super::errors::AuthenticationError; +use crate::headers::authorization; +use crate::headers::www_authenticate::bearer; +pub use crate::headers::www_authenticate::bearer::Error; /// [BearerAuth](./struct/BearerAuth.html) extractor configuration. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Config(bearer::Bearer); impl Config { @@ -18,7 +22,7 @@ impl Config { /// /// 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) -> &mut Config { + pub fn scope>>(mut self, value: T) -> Config { self.0.scope = Some(value.into()); self } @@ -27,12 +31,18 @@ impl Config { /// /// 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) -> &mut Config { + pub fn realm>>(mut self, value: T) -> Config { self.0.realm = Some(value.into()); self } } +impl AsRef for Config { + fn as_ref(&self) -> &bearer::Bearer { + &self.0 + } +} + impl ExtractorConfig for Config { type Inner = bearer::Bearer; @@ -41,60 +51,114 @@ impl ExtractorConfig for Config { } } -impl Default for Config { - fn default() -> Self { - Config(bearer::Bearer::default()) - } -} - /// Extractor for HTTP Bearer auth /// /// # Example /// /// ```rust -/// # extern crate actix_web; -/// # extern crate actix_web_httpauth; -/// use actix_web::Result; /// use actix_web_httpauth::extractors::bearer::BearerAuth; /// -/// fn index(auth: BearerAuth) -> Result { -/// Ok(format!("Hello, user with token {}!", auth.token())) +/// fn index(auth: BearerAuth) -> String { +/// format!("Hello, user with token {}!", auth.token()) +/// } +/// ``` +/// +/// If authentication fails, this extractor fetches the [`Config`] instance +/// from the [app data] in order to properly form the `WWW-Authenticate` response header. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::{web, App}; +/// use actix_web_httpauth::extractors::bearer::{BearerAuth, Config}; +/// +/// fn index(auth: BearerAuth) -> String { +/// format!("Hello, {}!", auth.token()) +/// } +/// +/// fn main() { +/// let app = App::new() +/// .data(Config::default().realm("Restricted area").scope("email photo")) +/// .service(web::resource("/index.html").route(web::get().to(index))); /// } /// ``` #[derive(Debug, Clone)] pub struct BearerAuth(authorization::Bearer); impl BearerAuth { + /// Returns bearer token provided by client. pub fn token(&self) -> &str { - self.0.token.as_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 { +impl FromRequest for BearerAuth { type Config = Config; - type Result = Result>; + type Future = Result; + type Error = AuthenticationError; - fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> ::Future { authorization::Authorization::::parse(req) - .map(|auth| BearerAuth(auth.into_inner())) - .map_err(|_| AuthenticationError::new(cfg.0.clone())) + .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. pub fn with_error(mut self, kind: Error) -> Self { *self.status_code_mut() = kind.status_code(); self.challenge_mut().error = Some(kind); self } - pub fn with_error_description>(mut self, desc: T) -> Self { + /// Attach error description to the current Authentication error. + pub fn with_error_description(mut self, desc: T) -> Self + where + T: Into>, + { self.challenge_mut().error_description = Some(desc.into()); self } - pub fn with_error_uri>(mut self, uri: T) -> Self { + /// Attach error URI to the current Authentication error. + /// + /// It is up to implementor to provide properly formed absolute URI. + pub fn with_error_uri(mut self, uri: T) -> Self + where + T: Into>, + { self.challenge_mut().error_uri = Some(uri.into()); self } diff --git a/src/extractors/config.rs b/src/extractors/config.rs index 9337bffa5..721cf8f5b 100644 --- a/src/extractors/config.rs +++ b/src/extractors/config.rs @@ -1,6 +1,5 @@ -use headers::www_authenticate::Challenge; - use super::AuthenticationError; +use crate::headers::www_authenticate::Challenge; pub trait ExtractorConfig { type Inner: Challenge; @@ -8,7 +7,10 @@ pub trait ExtractorConfig { fn into_inner(self) -> Self::Inner; } -impl From for AuthenticationError<::Inner> where T: ExtractorConfig { +impl From for AuthenticationError<::Inner> +where + T: ExtractorConfig, +{ fn from(config: T) -> Self { AuthenticationError::new(config.into_inner()) } diff --git a/src/extractors/errors.rs b/src/extractors/errors.rs index 3d161333b..1834f6b8a 100644 --- a/src/extractors/errors.rs +++ b/src/extractors/errors.rs @@ -1,14 +1,13 @@ -use std::str; -use std::fmt; use std::error::Error; +use std::fmt; -use actix_web::{HttpResponse, ResponseError}; use actix_web::http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; -use headers::www_authenticate::{WWWAuthenticate}; -use headers::www_authenticate::Challenge; +use crate::headers::www_authenticate::Challenge; +use crate::headers::www_authenticate::WwwAuthenticate; -/// Authentication error returned by Auth extractor. +/// Authentication error returned by authentication extractors. /// /// Different extractors may extend `AuthenticationError` implementation /// in order to provide access to inner challenge fields. @@ -19,6 +18,9 @@ pub struct AuthenticationError { } impl AuthenticationError { + /// Creates new authentication error from the provided `challenge`. + /// + /// By default returned error will resolve into the `HTTP 401` status code. pub fn new(challenge: C) -> AuthenticationError { AuthenticationError { challenge, @@ -26,10 +28,15 @@ impl AuthenticationError { } } + /// Returns mutable reference to the inner challenge instance. pub fn challenge_mut(&mut self) -> &mut C { &mut self.challenge } + /// Returns mutable reference to the inner status code. + /// + /// Can be used to override returned status code, but by default + /// this lib tries to stick to the RFC, so it might be unreasonable. pub fn status_code_mut(&mut self) -> &mut StatusCode { &mut self.status_code } @@ -37,27 +44,17 @@ impl AuthenticationError { impl fmt::Display for AuthenticationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let bytes = self.challenge.to_bytes(); - let repr = str::from_utf8(&bytes) - // Should not happen since challenges are crafted manually - // from `&'static str`'s and Strings - .map_err(|_| fmt::Error)?; - - f.write_str(repr) + fmt::Display::fmt(&self.status_code, f) } } -impl Error for AuthenticationError { - fn description(&self) -> &str { - unimplemented!() - } -} +impl Error for AuthenticationError {} impl ResponseError for AuthenticationError { fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code) // TODO: Get rid of the `.clone()` - .set(WWWAuthenticate(self.challenge.clone())) + .set(WwwAuthenticate(self.challenge.clone())) .finish() } } diff --git a/src/extractors/mod.rs b/src/extractors/mod.rs index be364ab26..797c99713 100644 --- a/src/extractors/mod.rs +++ b/src/extractors/mod.rs @@ -1,6 +1,8 @@ -mod errors; -mod config; +//! Type-safe authentication information extractors + pub mod basic; pub mod bearer; +mod config; +mod errors; pub use self::errors::AuthenticationError; diff --git a/src/headers/authorization/errors.rs b/src/headers/authorization/errors.rs index abdb80f9b..6ba240507 100644 --- a/src/headers/authorization/errors.rs +++ b/src/headers/authorization/errors.rs @@ -1,9 +1,8 @@ -use std::str; -use std::fmt; -use std::error::Error; use std::convert::From; +use std::error::Error; +use std::fmt; +use std::str; -use base64; use actix_web::http::header; /// Possible errors while parsing `Authorization` header. @@ -18,8 +17,11 @@ pub enum ParseError { MissingScheme, /// Required authentication field is missing MissingField(&'static str), + /// Unable to convert header into the str ToStrError(header::ToStrError), + /// Malformed base64 string Base64DecodeError(base64::DecodeError), + /// Malformed UTF-8 string Utf8Error(str::Utf8Error), } @@ -41,7 +43,7 @@ impl Error for ParseError { } } - fn cause(&self) -> Option<&Error> { + fn source(&self) -> Option<&(dyn Error + 'static)> { match self { ParseError::Invalid => None, ParseError::MissingScheme => None, diff --git a/src/headers/authorization/header.rs b/src/headers/authorization/header.rs index ab801f3bf..740af59b5 100644 --- a/src/headers/authorization/header.rs +++ b/src/headers/authorization/header.rs @@ -1,12 +1,10 @@ -use std::ops; use std::fmt; -use actix_web::{HttpMessage}; use actix_web::error::ParseError; use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION}; +use actix_web::HttpMessage; -use headers::authorization::scheme::Scheme; - +use crate::headers::authorization::scheme::Scheme; /// `Authorization` header, defined in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.2) /// @@ -21,27 +19,57 @@ use headers::authorization::scheme::Scheme; /// # Example /// /// ```rust -/// # extern crate actix_web; -/// # extern crate actix_web_httpauth; -/// -/// use actix_web::{HttpRequest, Result}; -/// use actix_web::http::header::Header; -/// use actix_web_httpauth::headers::authorization::{Authorization, Basic}; -/// +/// # use actix_web::http::header::Header; +/// # use actix_web::{HttpRequest, Result}; +/// # use actix_web_httpauth::headers::authorization::{Authorization, Basic}; /// fn handler(req: HttpRequest) -> Result { /// let auth = Authorization::::parse(&req)?; /// -/// Ok(format!("Hello, {}!", auth.username)) +/// Ok(format!("Hello, {}!", auth.as_ref().user_id())) /// } /// ``` +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)] pub struct Authorization(S); -impl Authorization { - pub fn into_inner(self) -> S { +impl Authorization +where + S: Scheme, +{ + /// Consumes `Authorization` header and returns inner [`Scheme`] implementation. + /// + /// [`Scheme`]: ./trait.Scheme.html + pub fn into_scheme(self) -> S { self.0 } } +impl From for Authorization +where + S: Scheme, +{ + fn from(scheme: S) -> Authorization { + Authorization(scheme) + } +} + +impl AsRef for Authorization +where + S: Scheme, +{ + fn as_ref(&self) -> &S { + &self.0 + } +} + +impl AsMut for Authorization +where + S: Scheme, +{ + fn as_mut(&mut self) -> &mut S { + &mut self.0 + } +} + impl Header for Authorization { #[inline] fn name() -> HeaderName { @@ -69,17 +97,3 @@ impl fmt::Display for Authorization { fmt::Display::fmt(&self.0, f) } } - -impl ops::Deref for Authorization { - type Target = S; - - fn deref(&self) -> &::Target { - &self.0 - } -} - -impl ops::DerefMut for Authorization { - fn deref_mut(&mut self) -> &mut ::Target { - &mut self.0 - } -} diff --git a/src/headers/authorization/mod.rs b/src/headers/authorization/mod.rs index d1420a5dc..b02e0531d 100644 --- a/src/headers/authorization/mod.rs +++ b/src/headers/authorization/mod.rs @@ -1,9 +1,11 @@ -mod scheme; -mod header; -mod errors; +//! `Authorization` header and various auth schemes + +mod errors; +mod header; +mod scheme; -pub use self::scheme::Scheme; -pub use self::scheme::basic::Basic; -pub use self::scheme::bearer::Bearer; pub use self::errors::ParseError; pub use self::header::Authorization; +pub use self::scheme::basic::Basic; +pub use self::scheme::bearer::Bearer; +pub use self::scheme::Scheme; diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 1af2868cf..06e8aabc2 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -1,18 +1,51 @@ -use std::str; +use std::borrow::Cow; use std::fmt; +use std::str; +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; use base64; use bytes::{BufMut, BytesMut}; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; -use headers::authorization::Scheme; -use headers::authorization::errors::ParseError; +use crate::headers::authorization::errors::ParseError; +use crate::headers::authorization::Scheme; +use crate::utils; /// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617) #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] pub struct Basic { - pub username: String, - pub password: Option, + user_id: Cow<'static, str>, + password: Option>, +} + +impl Basic { + /// Creates `Basic` credentials with provided `user_id` and optional `password`. + /// + /// ## Example + /// + /// ```rust + /// # use actix_web_httpauth::headers::authorization::Basic; + /// let credentials = Basic::new("Alladin", Some("open sesame")); + /// ``` + pub fn new(user_id: U, password: Option

) -> Basic + where + U: Into>, + P: Into>, + { + Basic { + user_id: user_id.into(), + password: password.map(Into::into), + } + } + + /// Returns client's user-ID. + pub fn user_id(&self) -> &Cow<'static, str> { + &self.user_id + } + + /// Returns client's password if provided. + pub fn password(&self) -> Option<&Cow<'static, str>> { + self.password.as_ref() + } } impl Scheme for Basic { @@ -29,24 +62,25 @@ impl Scheme for Basic { } let decoded = base64::decode(parts.next().ok_or(ParseError::Invalid)?)?; - let mut credentials = str::from_utf8(&decoded)? - .splitn(2, ':'); + let mut credentials = str::from_utf8(&decoded)?.splitn(2, ':'); - let username = credentials.next() - .ok_or(ParseError::MissingField("username")) - .map(|username| username.to_string())?; - let password = credentials.next() + let user_id = credentials + .next() + .ok_or(ParseError::MissingField("user_id")) + .map(|user_id| user_id.to_string().into())?; + let password = credentials + .next() .ok_or(ParseError::MissingField("password")) .map(|password| { if password.is_empty() { None } else { - Some(password.to_string()) + Some(password.to_string().into()) } })?; - Ok(Basic{ - username, + Ok(Basic { + user_id, password, }) } @@ -54,14 +88,13 @@ impl Scheme for Basic { impl fmt::Debug for Basic { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_fmt(format_args!("Basic {}:******", self.username)) + f.write_fmt(format_args!("Basic {}:******", self.user_id)) } } impl fmt::Display for Basic { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // TODO: Display password also - f.write_fmt(format_args!("Basic {}:******", self.username)) + f.write_fmt(format_args!("Basic {}:******", self.user_id)) } } @@ -69,13 +102,12 @@ impl IntoHeaderValue for Basic { type Error = InvalidHeaderValueBytes; fn try_into(self) -> Result::Error> { - let mut credentials = BytesMut::with_capacity( - self.username.len() + 1 + self.password.as_ref().map_or(0, |pwd| pwd.len()) - ); - credentials.put(&self.username); + 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 { - credentials.put(password); + utils::put_cow(&mut credentials, password); } // TODO: It would be nice not to allocate new `String` here but write directly to `value` @@ -90,8 +122,8 @@ impl IntoHeaderValue for Basic { #[cfg(test)] mod tests { + use super::{Basic, Scheme}; use actix_web::http::header::{HeaderValue, IntoHeaderValue}; - use super::{Scheme, Basic}; #[test] fn test_parse_header() { @@ -100,8 +132,8 @@ mod tests { assert!(scheme.is_ok()); let scheme = scheme.unwrap(); - assert_eq!(scheme.username, "Aladdin"); - assert_eq!(scheme.password, Some("open sesame".to_string())); + assert_eq!(scheme.user_id, "Aladdin"); + assert_eq!(scheme.password, Some("open sesame".into())); } #[test] @@ -111,7 +143,7 @@ mod tests { assert!(scheme.is_ok()); let scheme = scheme.unwrap(); - assert_eq!(scheme.username, "Aladdin"); + assert_eq!(scheme.user_id, "Aladdin"); assert_eq!(scheme.password, None); } @@ -150,17 +182,19 @@ mod tests { #[test] fn test_into_header_value() { let basic = Basic { - username: "Aladdin".to_string(), - password: Some("open sesame".to_string()), + user_id: "Aladdin".into(), + password: Some("open sesame".into()), }; let result = basic.try_into(); assert!(result.is_ok()); - assert_eq!(result.unwrap(), HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")); + assert_eq!( + result.unwrap(), + HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==") + ); } } - #[cfg(all(test, feature = "nightly"))] mod benches { use test::Bencher; @@ -172,17 +206,15 @@ mod benches { #[bench] fn bench_parsing(b: &mut Bencher) { let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); - b.iter(|| { - Basic::parse(&value) - }); + b.iter(|| Basic::parse(&value)); } #[bench] fn bench_serializing(b: &mut Bencher) { b.iter(|| { let basic = Basic { - username: "Aladdin".to_string(), - password: Some("open sesame".to_string()), + user_id: "Aladdin".into(), + password: Some("open sesame".into()), }; basic.try_into() diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs index 2a7257cd9..c350c1deb 100644 --- a/src/headers/authorization/scheme/bearer.rs +++ b/src/headers/authorization/scheme/bearer.rs @@ -1,17 +1,43 @@ +use std::borrow::Cow; use std::fmt; -use bytes::{BufMut, BytesMut}; use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use bytes::{BufMut, BytesMut}; -use headers::authorization::scheme::Scheme; -use headers::authorization::errors::ParseError; +use crate::headers::authorization::errors::ParseError; +use crate::headers::authorization::scheme::Scheme; +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. #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] pub struct Bearer { - pub token: String, + token: Cow<'static, str>, +} + +impl Bearer { + /// Creates new `Bearer` credentials with the token provided. + /// + /// ## Example + /// + /// ```rust + /// # use actix_web_httpauth::headers::authorization::Bearer; + /// let credentials = Bearer::new("mF_9.B5f-4.1JqM"); + /// ``` + pub fn new(token: T) -> Bearer + where + T: Into>, + { + Bearer { + token: token.into(), + } + } + + /// Gets reference to the credentials token. + pub fn token(&self) -> &Cow<'static, str> { + &self.token + } } impl Scheme for Bearer { @@ -29,8 +55,8 @@ impl Scheme for Bearer { let token = parts.next().ok_or(ParseError::Invalid)?; - Ok(Bearer{ - token: token.to_string(), + Ok(Bearer { + token: token.to_string().into(), }) } } @@ -53,7 +79,7 @@ impl IntoHeaderValue for Bearer { fn try_into(self) -> Result::Error> { let mut buffer = BytesMut::with_capacity(7 + self.token.len()); buffer.put("Bearer "); - buffer.put(self.token); + utils::put_cow(&mut buffer, &self.token); HeaderValue::from_shared(buffer.freeze()) } @@ -61,8 +87,8 @@ impl IntoHeaderValue for Bearer { #[cfg(test)] mod tests { + use super::{Bearer, Scheme}; use actix_web::http::header::{HeaderValue, IntoHeaderValue}; - use super::{Scheme, Bearer}; #[test] fn test_parse_header() { @@ -100,9 +126,7 @@ mod tests { #[test] fn test_into_header_value() { - let bearer = Bearer { - token: "mF_9.B5f-4.1JqM".to_string(), - }; + let bearer = Bearer::new("mF_9.B5f-4.1JqM"); let result = bearer.try_into(); assert!(result.is_ok()); diff --git a/src/headers/authorization/scheme/mod.rs b/src/headers/authorization/scheme/mod.rs index 42524129e..5832965f3 100644 --- a/src/headers/authorization/scheme/mod.rs +++ b/src/headers/authorization/scheme/mod.rs @@ -1,13 +1,14 @@ use std::fmt::{Debug, Display}; -use actix_web::http::header::{IntoHeaderValue, HeaderValue}; +use actix_web::http::header::{HeaderValue, IntoHeaderValue}; pub mod basic; pub mod bearer; -use headers::authorization::errors::ParseError; +use crate::headers::authorization::errors::ParseError; /// Authentication scheme for [`Authorization`](./struct.Authorization.html) header. -pub trait Scheme: IntoHeaderValue + Debug + Display + Clone + Send + Sync { +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/mod.rs b/src/headers/mod.rs index 0d0d12342..5e38e661a 100644 --- a/src/headers/mod.rs +++ b/src/headers/mod.rs @@ -1,2 +1,4 @@ +//! Typed HTTP headers + pub mod authorization; pub mod www_authenticate; diff --git a/src/headers/www_authenticate/challenge/basic.rs b/src/headers/www_authenticate/challenge/basic.rs index 492b2677c..c3c4f22cb 100644 --- a/src/headers/www_authenticate/challenge/basic.rs +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -1,20 +1,78 @@ -use std::str; -use std::fmt; -use std::default::Default; +//! Challenge for the "Basic" HTTP Authentication Scheme + +use std::borrow::Cow; +use std::default::Default; +use std::fmt; +use std::str; -use bytes::{BufMut, Bytes, BytesMut}; use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use bytes::{BufMut, Bytes, BytesMut}; use super::Challenge; +use crate::utils; -/// Challenge for `WWW-Authenticate` header with HTTP Basic auth scheme, +/// Challenge for [`WWW-Authenticate`] header with HTTP Basic auth scheme, /// described in [RFC 7617](https://tools.ietf.org/html/rfc7617) -#[derive(Debug, Clone)] +/// +/// ## Example +/// +/// ```rust +/// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; +/// use actix_web_httpauth::headers::www_authenticate::basic::Basic; +/// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate; +/// +/// fn index(_req: HttpRequest) -> HttpResponse { +/// let challenge = Basic::with_realm("Restricted area"); +/// +/// HttpResponse::Unauthorized().set(WwwAuthenticate(challenge)).finish() +/// } +/// ``` +/// +/// [`WWW-Authenticate`]: ../struct.WwwAuthenticate.html +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)] pub struct Basic { // "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A - pub realm: Option, + pub(crate) realm: Option>, } +impl Basic { + /// Creates new `Basic` challenge with an empty `realm` field. + /// + /// ## Example + /// + /// ```rust + /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; + /// let challenge = Basic::new(); + /// ``` + pub fn new() -> Basic { + Default::default() + } + + /// Creates new `Basic` challenge from the provided `realm` field value. + /// + /// ## Examples + /// + /// ```rust + /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; + /// let challenge = Basic::with_realm("Restricted area"); + /// ``` + /// + /// ```rust + /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; + /// let my_realm = "Earth realm".to_string(); + /// let challenge = Basic::with_realm(my_realm); + /// ``` + pub fn with_realm(value: T) -> Basic + where + T: Into>, + { + Basic { + realm: Some(value.into()), + } + } +} + +#[doc(hidden)] impl Challenge for Basic { fn to_bytes(&self) -> Bytes { // 5 is for `"Basic"`, 9 is for `"realm=\"\""` @@ -23,7 +81,7 @@ impl Challenge for Basic { buffer.put("Basic"); if let Some(ref realm) = self.realm { buffer.put(" realm=\""); - buffer.put(realm); + utils::put_cow(&mut buffer, realm); buffer.put("\""); } @@ -51,15 +109,6 @@ impl IntoHeaderValue for Basic { } } - -impl Default for Basic { - fn default() -> Self { - Self { - realm: None, - } - } -} - #[cfg(test)] mod tests { use super::Basic; @@ -80,7 +129,7 @@ mod tests { #[test] fn test_with_realm_into_header_value() { let challenge = Basic { - realm: Some("Restricted area".to_string()), + realm: Some("Restricted area".into()), }; let value = challenge.try_into(); diff --git a/src/headers/www_authenticate/challenge/bearer.rs b/src/headers/www_authenticate/challenge/bearer.rs deleted file mode 100644 index 04d75f4e8..000000000 --- a/src/headers/www_authenticate/challenge/bearer.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::str; -use std::fmt; -use std::default::Default; - -use bytes::{BufMut, Bytes, BytesMut}; -use actix_web::http::StatusCode; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; - -use super::Challenge; - -/// Bearer authorization error types, described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3.1) -#[derive(Debug, Copy, Clone)] -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. - InvalidRequest, - - /// 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. - InsufficientScope, -} - -impl Error { - pub fn status_code(&self) -> StatusCode { - match *self { - Error::InvalidRequest => StatusCode::BAD_REQUEST, - Error::InvalidToken => StatusCode::UNAUTHORIZED, - Error::InsufficientScope => StatusCode::FORBIDDEN, - } - } - - fn as_str(&self) -> &'static str { - match *self { - Error::InvalidRequest => "invalid_request", - Error::InvalidToken => "invalid_token", - Error::InsufficientScope => "insufficient_scope", - } - } -} - -/// Challenge for `WWW-Authenticate` header with HTTP Bearer auth scheme, -/// described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3) -#[derive(Debug, Clone)] -pub struct Bearer { - pub scope: Option, - pub realm: Option, - pub error: Option, - pub error_description: Option, - /// It is up to implementor to provide correct absolute URI - pub error_uri: Option, -} - -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) + - 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) + - self.scope.as_ref().map_or(0, |scope| scope.len() + 9) + - desc_uri_required; - let mut buffer = BytesMut::with_capacity(capacity); - buffer.put("Bearer"); - - if let Some(ref realm) = self.realm { - buffer.put(" realm=\""); - buffer.put(realm); - buffer.put("\""); - } - - if let Some(ref scope) = self.scope { - buffer.put(" scope=\""); - buffer.put(scope); - buffer.put("\""); - } - - if let Some(ref error) = self.error { - let error_repr = error.as_str(); - let remaining = buffer.remaining_mut(); - let required = desc_uri_required + error_repr.len() + 9; // 9 is for `" error=\"\""` - if remaining < required { - buffer.reserve(required); - } - buffer.put(" error=\""); - buffer.put(error_repr); - buffer.put("\"") - } - - if let Some(ref error_description) = self.error_description { - buffer.put(" error_description=\""); - buffer.put(error_description); - buffer.put("\""); - } - - if let Some(ref error_uri) = self.error_uri { - buffer.put(" error_uri=\""); - buffer.put(error_uri); - buffer.put("\""); - } - - buffer.freeze() - } -} - -impl Default for Bearer { - fn default() -> Self { - Bearer { - scope: None, - realm: None, - error: None, - error_description: None, - error_uri: None, - } - } -} - -impl fmt::Display for Bearer { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - let bytes = self.to_bytes(); - let repr = str::from_utf8(&bytes) - // Should not happen since challenges are crafted manually - // from `&'static str`'s and Strings - .map_err(|_| fmt::Error)?; - - f.write_str(repr) - } -} - -impl IntoHeaderValue for Bearer { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result::Error> { - HeaderValue::from_shared(self.to_bytes()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn to_bytes() { - let b = Bearer { - scope: None, - realm: None, - error: Some(Error::InvalidToken), - error_description: Some(String::from("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found")), - error_uri: None, - }; - assert_eq!("Bearer error=\"invalid_token\" error_description=\"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found\"", - format!("{}", b)); - } -} diff --git a/src/headers/www_authenticate/challenge/bearer/builder.rs b/src/headers/www_authenticate/challenge/bearer/builder.rs new file mode 100644 index 000000000..b4bf11455 --- /dev/null +++ b/src/headers/www_authenticate/challenge/bearer/builder.rs @@ -0,0 +1,63 @@ +use std::borrow::Cow; + +use super::{Bearer, Error}; + +/// Builder for the [`Bearer`] challenge. +/// +/// It is up to implementor to fill all required fields, +/// neither this `Builder` or [`Bearer`] does not provide any validation. +/// +/// [`Bearer`]: struct.Bearer.html +#[derive(Debug, Default)] +pub struct BearerBuilder(Bearer); + +impl BearerBuilder { + /// Provides the `scope` attribute, as defined in [RFC6749, Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3) + pub fn scope(mut self, value: T) -> Self + where + T: Into>, + { + self.0.scope = Some(value.into()); + self + } + + /// Provides the `realm` attribute, as defined in [RFC2617](https://tools.ietf.org/html/rfc2617) + pub fn realm(mut self, value: T) -> Self + where + T: Into>, + { + self.0.realm = Some(value.into()); + self + } + + /// Provides the `error` attribute, as defined in [RFC6750, Section 3.1](https://tools.ietf.org/html/rfc6750#section-3.1) + pub fn error(mut self, value: Error) -> Self { + self.0.error = Some(value); + self + } + + /// Provides the `error_description` attribute, as defined in [RFC6750, Section 3](https://tools.ietf.org/html/rfc6750#section-3) + pub fn error_description(mut self, value: T) -> Self + where + T: Into>, + { + self.0.error_description = Some(value.into()); + self + } + + /// Provides the `error_uri` attribute, as defined in [RFC6750, Section 3](https://tools.ietf.org/html/rfc6750#section-3) + /// + /// It is up to implementor to provide properly-formed absolute URI. + pub fn error_uri(mut self, value: T) -> Self + where + T: Into>, + { + self.0.error_uri = Some(value.into()); + self + } + + /// Consumes the builder and returns built `Bearer` instance. + pub fn finish(self) -> Bearer { + self.0 + } +} diff --git a/src/headers/www_authenticate/challenge/bearer/challenge.rs b/src/headers/www_authenticate/challenge/bearer/challenge.rs new file mode 100644 index 000000000..8a03372f3 --- /dev/null +++ b/src/headers/www_authenticate/challenge/bearer/challenge.rs @@ -0,0 +1,132 @@ +use std::borrow::Cow; +use std::fmt; +use std::str; + +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use bytes::{BufMut, Bytes, BytesMut}; + +use super::super::Challenge; +use super::{BearerBuilder, Error}; +use crate::utils; + +/// Challenge for [`WWW-Authenticate`] header with HTTP Bearer auth scheme, +/// described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3) +/// +/// ## Example +/// +/// ```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::WwwAuthenticate; +/// +/// fn index(_req: HttpRequest) -> HttpResponse { +/// let challenge = Bearer::build() +/// .realm("example") +/// .scope("openid profile email") +/// .error(Error::InvalidToken) +/// .error_description("The access token expired") +/// .error_uri("http://example.org") +/// .finish(); +/// +/// HttpResponse::Unauthorized().set(WwwAuthenticate(challenge)).finish() +/// } +/// ``` +/// +/// [`WWW-Authenticate`]: ../struct.WwwAuthenticate.html +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)] +pub struct Bearer { + pub(crate) scope: Option>, + pub(crate) realm: Option>, + pub(crate) error: Option, + pub(crate) error_description: Option>, + pub(crate) error_uri: Option>, +} + +impl Bearer { + /// Creates the builder for `Bearer` challenge. + /// + /// ## Example + /// + /// ```rust + /// # use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer}; + /// let challenge = Bearer::build() + /// .realm("Restricted area") + /// .scope("openid profile email") + /// .finish(); + /// ``` + pub fn build() -> BearerBuilder { + BearerBuilder::default() + } +} + +#[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) + + 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) + + self.scope.as_ref().map_or(0, |scope| scope.len() + 9) + + desc_uri_required; + let mut buffer = BytesMut::with_capacity(capacity); + buffer.put("Bearer"); + + if let Some(ref realm) = self.realm { + buffer.put(" realm=\""); + utils::put_cow(&mut buffer, realm); + buffer.put("\""); + } + + if let Some(ref scope) = self.scope { + buffer.put(" scope=\""); + utils::put_cow(&mut buffer, scope); + buffer.put("\""); + } + + if let Some(ref error) = self.error { + let error_repr = error.as_str(); + let remaining = buffer.remaining_mut(); + let required = desc_uri_required + error_repr.len() + 9; // 9 is for `" error=\"\""` + if remaining < required { + buffer.reserve(required); + } + buffer.put(" error=\""); + buffer.put(error_repr); + buffer.put("\"") + } + + if let Some(ref error_description) = self.error_description { + buffer.put(" error_description=\""); + utils::put_cow(&mut buffer, error_description); + buffer.put("\""); + } + + if let Some(ref error_uri) = self.error_uri { + buffer.put(" error_uri=\""); + utils::put_cow(&mut buffer, error_uri); + buffer.put("\""); + } + + buffer.freeze() + } +} + +impl fmt::Display for Bearer { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let bytes = self.to_bytes(); + let repr = str::from_utf8(&bytes) + // Should not happen since challenges are crafted manually + // from `&'static str`'s and Strings + .map_err(|_| fmt::Error)?; + + f.write_str(repr) + } +} + +impl IntoHeaderValue for Bearer { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result::Error> { + HeaderValue::from_shared(self.to_bytes()) + } +} diff --git a/src/headers/www_authenticate/challenge/bearer/errors.rs b/src/headers/www_authenticate/challenge/bearer/errors.rs new file mode 100644 index 000000000..5509fccec --- /dev/null +++ b/src/headers/www_authenticate/challenge/bearer/errors.rs @@ -0,0 +1,48 @@ +use std::fmt; + +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. + InvalidRequest, + + /// 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. + InsufficientScope, +} + +impl Error { + /// Returns [HTTP status code] suitable for current error type. + /// + /// [HTTP status code]: `actix_web::http::StatusCode` + #[allow(clippy::trivially_copy_pass_by_ref)] + pub fn status_code(&self) -> StatusCode { + match self { + Error::InvalidRequest => StatusCode::BAD_REQUEST, + Error::InvalidToken => StatusCode::UNAUTHORIZED, + Error::InsufficientScope => StatusCode::FORBIDDEN, + } + } + + #[doc(hidden)] + #[allow(clippy::trivially_copy_pass_by_ref)] + pub fn as_str(&self) -> &str { + match self { + Error::InvalidRequest => "invalid_request", + Error::InvalidToken => "invalid_token", + Error::InsufficientScope => "insufficient_scope", + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.as_str()) + } +} diff --git a/src/headers/www_authenticate/challenge/bearer/mod.rs b/src/headers/www_authenticate/challenge/bearer/mod.rs new file mode 100644 index 000000000..d51237c45 --- /dev/null +++ b/src/headers/www_authenticate/challenge/bearer/mod.rs @@ -0,0 +1,12 @@ +//! Challenge for the "Bearer" HTTP Authentication Scheme + +mod builder; +mod challenge; +mod errors; + +pub use self::builder::BearerBuilder; +pub use self::challenge::Bearer; +pub use self::errors::Error; + +#[cfg(test)] +mod tests; diff --git a/src/headers/www_authenticate/challenge/bearer/tests.rs b/src/headers/www_authenticate/challenge/bearer/tests.rs new file mode 100644 index 000000000..03f088e17 --- /dev/null +++ b/src/headers/www_authenticate/challenge/bearer/tests.rs @@ -0,0 +1,14 @@ +use super::*; + +#[test] +fn to_bytes() { + let b = Bearer::build() + .error(Error::InvalidToken) + .error_description("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found") + .finish(); + + assert_eq!( + "Bearer error=\"invalid_token\" error_description=\"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found\"", + format!("{}", b) + ); +} diff --git a/src/headers/www_authenticate/challenge/mod.rs b/src/headers/www_authenticate/challenge/mod.rs index 372de9884..ce70f4fe2 100644 --- a/src/headers/www_authenticate/challenge/mod.rs +++ b/src/headers/www_authenticate/challenge/mod.rs @@ -1,12 +1,13 @@ use std::fmt::{Debug, Display}; -use bytes::Bytes; use actix_web::http::header::IntoHeaderValue; +use bytes::Bytes; 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 d439ea97b..6f08c935f 100644 --- a/src/headers/www_authenticate/header.rs +++ b/src/headers/www_authenticate/header.rs @@ -1,36 +1,18 @@ -use actix_web::{HttpMessage}; use actix_web::error::ParseError; use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE}; +use actix_web::HttpMessage; use super::Challenge; /// `WWW-Authenticate` header, described in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.1) /// -/// `WWW-Authenticate` header is generic over [Challenge](./trait.Challenge.html) -/// -/// # Example -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate actix_web_httpauth; -/// -/// use actix_web::{HttpRequest, HttpResponse}; -/// use actix_web::http::StatusCode; -/// use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate}; -/// use actix_web_httpauth::headers::www_authenticate::basic::Basic; -/// -/// fn handler(req: HttpRequest) -> HttpResponse { -/// let challenge = Basic { -/// realm: Some("Restricted area".to_string()), -/// }; -/// req.build_response(StatusCode::UNAUTHORIZED) -/// .set(WWWAuthenticate(challenge)) -/// .finish() -/// } -/// ``` -pub struct WWWAuthenticate(pub C); +/// 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. +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)] +pub struct WwwAuthenticate(pub C); -impl Header for WWWAuthenticate { +impl Header for WwwAuthenticate { fn name() -> HeaderName { WWW_AUTHENTICATE } @@ -40,7 +22,7 @@ impl Header for WWWAuthenticate { } } -impl IntoHeaderValue for WWWAuthenticate { +impl IntoHeaderValue for WwwAuthenticate { type Error = ::Error; fn try_into(self) -> Result::Error> { diff --git a/src/headers/www_authenticate/mod.rs b/src/headers/www_authenticate/mod.rs index 3c874a48a..6262f050a 100644 --- a/src/headers/www_authenticate/mod.rs +++ b/src/headers/www_authenticate/mod.rs @@ -1,7 +1,9 @@ +//! `WWW-Authenticate` header and various auth challenges + mod challenge; mod header; -pub use self::header::WWWAuthenticate; -pub use self::challenge::Challenge; pub use self::challenge::basic; pub use self::challenge::bearer; +pub use self::challenge::Challenge; +pub use self::header::WwwAuthenticate; diff --git a/src/lib.rs b/src/lib.rs index f97de92c2..8ada78c6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,21 @@ //! HTTP Authorization support for [actix-web](https://actix.rs) framework. //! -//! Provides [`Authorization`](./headers/authorization/struct.Authorization.html) -//! and [`WWW-Authenticate`](./headers/www_authenticate/struct.WWWAuthenticate.html) headers, -//! and `actix-web` extractors for an `Authorization` header. +//! Provides [Authorization] and [WWW-Authenticate] headers, +//! and [extractors] for an [Authorization] header. +//! +//! ## Supported schemes +//! +//! * `Basic`, as defined in [RFC7617](https://tools.ietf.org/html/rfc7617) +//! * `Bearer`, as defined in [RFC6750](https://tools.ietf.org/html/rfc6750) +//! +//! [Authorization]: `crate::headers::authorization::Authorization` +//! [WWW-Authenticate]: `crate::headers::www_authenticate::WwwAuthenticate` +//! [extractors]: https://actix.rs/docs/extractors/ +#![forbid(bare_trait_objects)] +#![forbid(missing_docs)] #![cfg_attr(feature = "nightly", feature(test))] -#[cfg(feature = "nightly")] extern crate test; -extern crate actix_web; -extern crate bytes; -extern crate base64; - -pub mod headers; pub mod extractors; +pub mod headers; +mod utils; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 000000000..e6b7297e9 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,13 @@ +use std::borrow::Cow; + +use bytes::{BufMut, BytesMut}; + +// `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>) { + match value { + Cow::Borrowed(str) => buf.put(str), + Cow::Owned(ref string) => buf.put(string), + } +}