From 716bffeb8e32c6432095fdaac46d684767052267 Mon Sep 17 00:00:00 2001 From: svartalf Date: Wed, 30 May 2018 16:43:39 +0300 Subject: [PATCH] Bearer auth --- .travis.yml | 1 + Cargo.toml | 15 +- README.md | 1 + examples/basic.rs | 40 ---- examples/extractor_basic.rs | 36 ++++ examples/extractor_bearer.rs | 40 ++++ examples/header_www_authenticate_basic.rs | 25 +++ examples/header_www_authenticate_bearer.rs | 29 +++ src/basic/config.rs | 57 ------ src/basic/mod.rs | 101 --------- src/basic/tests.rs | 56 ----- src/errors.rs | 41 ---- src/extractors/basic.rs | 80 ++++++++ src/extractors/bearer.rs | 101 +++++++++ src/extractors/config.rs | 15 ++ src/extractors/errors.rs | 63 ++++++ src/extractors/mod.rs | 6 + src/headers/authorization/errors.rs | 70 +++++++ src/headers/authorization/header.rs | 85 ++++++++ src/headers/authorization/mod.rs | 9 + src/headers/authorization/scheme/basic.rs | 191 ++++++++++++++++++ src/headers/authorization/scheme/bearer.rs | 111 ++++++++++ src/headers/authorization/scheme/mod.rs | 13 ++ src/headers/mod.rs | 2 + .../www_authenticate/challenge/basic.rs | 91 +++++++++ .../www_authenticate/challenge/bearer.rs | 136 +++++++++++++ src/headers/www_authenticate/challenge/mod.rs | 12 ++ src/headers/www_authenticate/header.rs | 49 +++++ src/headers/www_authenticate/mod.rs | 7 + src/lib.rs | 17 +- 30 files changed, 1192 insertions(+), 308 deletions(-) delete mode 100644 examples/basic.rs create mode 100644 examples/extractor_basic.rs create mode 100644 examples/extractor_bearer.rs create mode 100644 examples/header_www_authenticate_basic.rs create mode 100644 examples/header_www_authenticate_bearer.rs delete mode 100644 src/basic/config.rs delete mode 100644 src/basic/mod.rs delete mode 100644 src/basic/tests.rs delete mode 100644 src/errors.rs create mode 100644 src/extractors/basic.rs create mode 100644 src/extractors/bearer.rs create mode 100644 src/extractors/config.rs create mode 100644 src/extractors/errors.rs create mode 100644 src/extractors/mod.rs create mode 100644 src/headers/authorization/errors.rs create mode 100644 src/headers/authorization/header.rs create mode 100644 src/headers/authorization/mod.rs create mode 100644 src/headers/authorization/scheme/basic.rs create mode 100644 src/headers/authorization/scheme/bearer.rs create mode 100644 src/headers/authorization/scheme/mod.rs create mode 100644 src/headers/mod.rs create mode 100644 src/headers/www_authenticate/challenge/basic.rs create mode 100644 src/headers/www_authenticate/challenge/bearer.rs create mode 100644 src/headers/www_authenticate/challenge/mod.rs create mode 100644 src/headers/www_authenticate/header.rs create mode 100644 src/headers/www_authenticate/mod.rs diff --git a/.travis.yml b/.travis.yml index 8c91a7415..45793de3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: rust +cache: cargo rust: - stable - beta diff --git a/Cargo.toml b/Cargo.toml index e88f6bd1e..3bb8ce0f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.0.2" +version = "0.0.3" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -12,11 +12,14 @@ categories = ["web-programming::http-server"] license = "MIT/Apache-2.0" exclude = [".travis.yml", ".gitignore"] -[badges] -travis-ci = { repository = "svartalf/actix-web-httpauth", branch = "master" } - [dependencies] actix-web = "0.6" +bytes = "0.4" base64 = "0.9" -percent-encoding = "1.0.1" -bytes = "0.4.7" + +[features] +default = [] +nightly = [] + +[badges] +travis-ci = { repository = "svartalf/actix-web-httpauth", branch = "master" } diff --git a/README.md b/README.md index 7b199822a..e64d18105 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,4 @@ and can be used both in middlewares and request handlers, check the `examples/` ## Supported schemes * [Basic](https://tools.ietf.org/html/rfc7617) + * [Bearer](https://tools.ietf.org/html/rfc6750) diff --git a/examples/basic.rs b/examples/basic.rs deleted file mode 100644 index 3a32eb938..000000000 --- a/examples/basic.rs +++ /dev/null @@ -1,40 +0,0 @@ -extern crate actix_web; -extern crate actix_web_httpauth; - -use actix_web::{server, App, HttpRequest, FromRequest, Result}; -use actix_web::middleware::{Middleware, Started}; -use actix_web_httpauth::basic::{BasicAuth, Config}; - -struct AuthMiddleware; - -impl Middleware for AuthMiddleware { - fn start(&self, req: &mut HttpRequest) -> Result { - let mut config = Config::default(); - config.realm("Restricted area".to_string()); - let auth = BasicAuth::from_request(&req, &config)?; - - // Please note that this is only an example, - // do not ever hardcode your credentials! - if auth.username == "root" && auth.password == "pass" { - Ok(Started::Done) - } else { - let response = BasicAuth::error_response(&config); - Ok(Started::Response(response)) - } - } - -} - -fn index(auth: BasicAuth) -> String { - format!("Hello, {}", auth.username) -} - -fn main() { - server::new(|| App::new() - // Comment the `.middleware()` line and let `BasicAuth` extractor - // in the `index` handler do the authentication routine - .middleware(AuthMiddleware) - .resource("/", |r| r.with(index))) - .bind("127.0.0.1:8088").unwrap() - .run(); -} diff --git a/examples/extractor_basic.rs b/examples/extractor_basic.rs new file mode 100644 index 000000000..8e63ae397 --- /dev/null +++ b/examples/extractor_basic.rs @@ -0,0 +1,36 @@ +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: &mut 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 new file mode 100644 index 000000000..145abecca --- /dev/null +++ b/examples/extractor_bearer.rs @@ -0,0 +1,40 @@ +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: &mut 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 new file mode 100644 index 000000000..031948b20 --- /dev/null +++ b/examples/header_www_authenticate_basic.rs @@ -0,0 +1,25 @@ +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 new file mode 100644 index 000000000..65d24b822 --- /dev/null +++ b/examples/header_www_authenticate_bearer.rs @@ -0,0 +1,29 @@ +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/src/basic/config.rs b/src/basic/config.rs deleted file mode 100644 index a11816c7a..000000000 --- a/src/basic/config.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::default::Default; - -use bytes::Bytes; -use percent_encoding; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue}; - -/// Challenge configuration for [BasicAuth](./struct.BasicAuth.html) extractor. -#[derive(Debug, Clone)] -pub struct Config { - // "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A - realm: Option, -} - -impl Config { - pub fn realm(&mut self, value: String) -> &mut Self { - self.realm = Some(value); - self - } - - fn as_bytes(&self) -> Bytes { - let mut bytes = Bytes::from_static(b"Basic"); - if let Some(ref realm) = self.realm { - bytes.extend_from_slice(b" realm=\""); - let realm = percent_encoding::utf8_percent_encode(realm, percent_encoding::SIMPLE_ENCODE_SET); - for part in realm { - bytes.extend_from_slice(part.as_bytes()); - } - bytes.extend_from_slice(b"\""); - } - - bytes - } -} - -impl IntoHeaderValue for Config { - type Error = InvalidHeaderValue; - - fn try_into(self) -> Result::Error> { - HeaderValue::from_bytes(&self.as_bytes()) - } -} - -impl<'a> IntoHeaderValue for &'a Config { - type Error = InvalidHeaderValue; - - fn try_into(self) -> Result::Error> { - HeaderValue::from_bytes(&self.as_bytes()) - } -} - -impl Default for Config { - fn default() -> Self { - Config { - realm: None, - } - } -} diff --git a/src/basic/mod.rs b/src/basic/mod.rs deleted file mode 100644 index c2c508d0a..000000000 --- a/src/basic/mod.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::string; -use std::convert::From; - -use base64; -use actix_web::{HttpRequest, HttpMessage, HttpResponse, FromRequest, ResponseError}; -use actix_web::http::header; - -mod config; - -use errors::Error; -pub use self::config::Config; - -/// Extractor for `Authorization: Basic {payload}` HTTP request header. -/// -/// If header is not present or malformed, `HTTP 401` response will be returned. -/// See [Config](./struct.Config.html) struct also. -/// -/// # Example -/// -/// As a handler-level extractor: -/// -/// ```rust -/// use actix_web_httpauth::basic::BasicAuth; -/// -/// pub fn handler(auth: BasicAuth) -> String { -/// format!("Hello, {}", auth.username) -/// } -/// ``` -/// -/// See `examples/basic.rs` file in sources -#[derive(Debug, PartialEq)] -pub struct BasicAuth { - pub username: String, - pub password: String, -} - -impl BasicAuth { - pub fn error_response(cfg: &Config) -> HttpResponse { - Error::new(cfg.clone()).error_response() - } - - fn parse(req: &HttpRequest) -> Result { - let header = req.headers().get(header::AUTHORIZATION) - .ok_or(ParseError)? - .to_str()?; - let mut parts = header.splitn(2, ' '); - - // Authorization mechanism - match parts.next() { - Some(mechanism) if mechanism == "Basic" => (), - _ => return Err(ParseError), - } - - // Authorization payload - let payload = parts.next().ok_or(ParseError)?; - let payload = base64::decode(payload)?; - let payload = String::from_utf8(payload)?; - let mut parts = payload.splitn(2, ':'); - let user = parts.next().ok_or(ParseError)?; - let password = parts.next().ok_or(ParseError)?; - - Ok(BasicAuth{ - username: user.to_string(), - password: password.to_string(), - }) - } -} - - -impl FromRequest for BasicAuth { - type Config = Config; - type Result = Result; - - fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { - BasicAuth::parse(req).map_err(|_| Error::new(cfg.clone())) - } -} - -#[derive(Debug)] -struct ParseError; - -impl From for ParseError { - fn from(_: base64::DecodeError) -> Self { - Self{} - } -} - -impl From for ParseError { - fn from(_: header::ToStrError) -> Self { - Self{} - } -} - -impl From for ParseError { - fn from(_: string::FromUtf8Error) -> Self { - Self{} - } -} - -#[cfg(test)] -mod tests; diff --git a/src/basic/tests.rs b/src/basic/tests.rs deleted file mode 100644 index 25b9436fb..000000000 --- a/src/basic/tests.rs +++ /dev/null @@ -1,56 +0,0 @@ -use base64; -use actix_web::FromRequest; -use actix_web::test::TestRequest; - -use super::BasicAuth; - -#[test] -fn test_valid_auth() { - let value = format!("Basic {}", base64::encode("user:pass")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_ok()); - let auth = auth.unwrap(); - assert_eq!(auth.username, "user".to_string()); - assert_eq!(auth.password, "pass".to_string()); -} - -#[test] -fn test_missing_header() { - let req = TestRequest::default().finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_err()); -} - -#[test] -fn test_invalid_mechanism() { - let value = format!("Digest {}", base64::encode("user:pass")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_err()); -} - -#[test] -fn test_invalid_format() { - let value = format!("Basic {}", base64::encode("user")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_err()); -} - -#[test] -fn test_user_without_password() { - let value = format!("Basic {}", base64::encode("user:")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_ok()); - assert_eq!(auth.unwrap(), BasicAuth { - username: "user".to_string(), - password: "".to_string(), - }) -} diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index 22be9ad6d..000000000 --- a/src/errors.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::fmt; -use std::error::Error as StdError; - -use actix_web::HttpResponse; -use actix_web::error::ResponseError; -use actix_web::http::{StatusCode, header}; - -use basic::Config; - -#[derive(Debug)] -pub struct Error { - challenge: Config, -} - -impl Error { - pub fn new(config: Config) -> Error { - Error { - challenge: config, - } - } -} - -impl ResponseError for Error { - fn error_response(&self) -> HttpResponse { - HttpResponse::build(StatusCode::UNAUTHORIZED) - .header(header::WWW_AUTHENTICATE, &self.challenge) - .finish() - } -} - -impl StdError for Error { - fn description(&self) -> &str { - "Unauthorized request" - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.description()) - } -} diff --git a/src/extractors/basic.rs b/src/extractors/basic.rs new file mode 100644 index 000000000..484058bb6 --- /dev/null +++ b/src/extractors/basic.rs @@ -0,0 +1,80 @@ +use std::default::Default; + +use actix_web::{HttpRequest, FromRequest}; +use actix_web::http::header::Header; + +use headers::authorization::{Authorization, Basic}; +use headers::www_authenticate::basic::Basic as Challenge; +use super::errors::AuthenticationError; +use super::config::ExtractorConfig; + +/// [`BasicAuth`](./struct.BasicAuth.html) extractor configuration, +/// used for `WWW-Authenticate` header later. +#[derive(Debug, Clone)] +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). + pub fn realm>(&mut self, value: T) -> &mut Config { + self.0.realm = Some(value.into()); + self + } +} + +impl ExtractorConfig for Config { + type Inner = Challenge; + + fn into_inner(self) -> Self::Inner { + self.0 + } +} + +impl Default for Config { + fn default() -> Self { + Config(Challenge::default()) + } +} + +/// 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())) +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct BasicAuth(Basic); + +impl BasicAuth { + pub fn username(&self) -> &str { + self.0.username.as_str() + } + + pub fn password(&self) -> Option<&str> { + match self.0.password { + None => None, + Some(ref pwd) => Some(pwd.as_str()) + } + } +} + +impl FromRequest for BasicAuth { + type Config = Config; + type Result = Result>; + + fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { + Authorization::::parse(req) + .map(|auth| BasicAuth(auth.into_inner())) + .map_err(|_| AuthenticationError::new(cfg.0.clone())) + } +} diff --git a/src/extractors/bearer.rs b/src/extractors/bearer.rs new file mode 100644 index 000000000..b3c9a5182 --- /dev/null +++ b/src/extractors/bearer.rs @@ -0,0 +1,101 @@ +use std::default::Default; + +use actix_web::{HttpRequest, FromRequest}; +use actix_web::http::header::Header; + +use headers::authorization; +use headers::www_authenticate::bearer; +pub use headers::www_authenticate::bearer::Error; +use super::errors::AuthenticationError; +use super::config::ExtractorConfig; + +/// [BearerAuth](./struct/BearerAuth.html) extractor configuration. +#[derive(Debug, Clone)] +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. + pub fn scope>(&mut self, value: T) -> &mut Config { + self.0.scope = Some(value.into()); + self + } + + /// 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). + pub fn realm>(&mut self, value: T) -> &mut Config { + self.0.realm = Some(value.into()); + self + } +} + +impl ExtractorConfig for Config { + type Inner = bearer::Bearer; + + fn into_inner(self) -> Self::Inner { + self.0 + } +} + +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())) +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct BearerAuth(authorization::Bearer); + +impl BearerAuth { + pub fn token(&self) -> &str { + self.0.token.as_str() + } +} + +impl FromRequest for BearerAuth { + type Config = Config; + type Result = Result>; + + fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { + authorization::Authorization::::parse(req) + .map(|auth| BearerAuth(auth.into_inner())) + .map_err(|_| AuthenticationError::new(cfg.0.clone())) + } +} + +/// Extended error customization for HTTP `Bearer` auth. +impl AuthenticationError { + 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 { + self.challenge_mut().error_description = Some(desc.into()); + self + } + + pub fn with_error_uri>(mut self, uri: T) -> Self { + self.challenge_mut().error_uri = Some(uri.into()); + self + } +} diff --git a/src/extractors/config.rs b/src/extractors/config.rs new file mode 100644 index 000000000..9337bffa5 --- /dev/null +++ b/src/extractors/config.rs @@ -0,0 +1,15 @@ +use headers::www_authenticate::Challenge; + +use super::AuthenticationError; + +pub trait ExtractorConfig { + type Inner: Challenge; + + fn into_inner(self) -> Self::Inner; +} + +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 new file mode 100644 index 000000000..3d161333b --- /dev/null +++ b/src/extractors/errors.rs @@ -0,0 +1,63 @@ +use std::str; +use std::fmt; +use std::error::Error; + +use actix_web::{HttpResponse, ResponseError}; +use actix_web::http::StatusCode; + +use headers::www_authenticate::{WWWAuthenticate}; +use headers::www_authenticate::Challenge; + +/// Authentication error returned by Auth extractor. +/// +/// Different extractors may extend `AuthenticationError` implementation +/// in order to provide access to inner challenge fields. +#[derive(Debug)] +pub struct AuthenticationError { + challenge: C, + status_code: StatusCode, +} + +impl AuthenticationError { + pub fn new(challenge: C) -> AuthenticationError { + AuthenticationError { + challenge, + status_code: StatusCode::UNAUTHORIZED, + } + } + + pub fn challenge_mut(&mut self) -> &mut C { + &mut self.challenge + } + + pub fn status_code_mut(&mut self) -> &mut StatusCode { + &mut self.status_code + } +} + +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) + } +} + +impl Error for AuthenticationError { + fn description(&self) -> &str { + unimplemented!() + } +} + +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())) + .finish() + } +} diff --git a/src/extractors/mod.rs b/src/extractors/mod.rs new file mode 100644 index 000000000..be364ab26 --- /dev/null +++ b/src/extractors/mod.rs @@ -0,0 +1,6 @@ +mod errors; +mod config; +pub mod basic; +pub mod bearer; + +pub use self::errors::AuthenticationError; diff --git a/src/headers/authorization/errors.rs b/src/headers/authorization/errors.rs new file mode 100644 index 000000000..abdb80f9b --- /dev/null +++ b/src/headers/authorization/errors.rs @@ -0,0 +1,70 @@ +use std::str; +use std::fmt; +use std::error::Error; +use std::convert::From; + +use base64; +use actix_web::http::header; + +/// Possible errors while parsing `Authorization` header. +/// +/// Should not be used directly unless you are implementing +/// your own [authentication scheme](./trait.Scheme.html). +#[derive(Debug)] +pub enum ParseError { + /// Header value is malformed + Invalid, + /// Authentication scheme is missing + MissingScheme, + /// Required authentication field is missing + MissingField(&'static str), + ToStrError(header::ToStrError), + Base64DecodeError(base64::DecodeError), + Utf8Error(str::Utf8Error), +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.description()) + } +} + +impl Error for ParseError { + fn description(&self) -> &str { + match self { + ParseError::Invalid => "Invalid header value", + ParseError::MissingScheme => "Missing authorization scheme", + ParseError::MissingField(_) => "Missing header field", + ParseError::ToStrError(e) => e.description(), + ParseError::Base64DecodeError(e) => e.description(), + ParseError::Utf8Error(e) => e.description(), + } + } + + fn cause(&self) -> Option<&Error> { + match self { + ParseError::Invalid => None, + ParseError::MissingScheme => None, + ParseError::MissingField(_) => None, + ParseError::ToStrError(e) => Some(e), + ParseError::Base64DecodeError(e) => Some(e), + ParseError::Utf8Error(e) => Some(e), + } + } +} + +impl From for ParseError { + fn from(e: header::ToStrError) -> Self { + ParseError::ToStrError(e) + } +} +impl From for ParseError { + fn from(e: base64::DecodeError) -> Self { + ParseError::Base64DecodeError(e) + } +} +impl From for ParseError { + fn from(e: str::Utf8Error) -> Self { + ParseError::Utf8Error(e) + } +} diff --git a/src/headers/authorization/header.rs b/src/headers/authorization/header.rs new file mode 100644 index 000000000..ab801f3bf --- /dev/null +++ b/src/headers/authorization/header.rs @@ -0,0 +1,85 @@ +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 headers::authorization::scheme::Scheme; + + +/// `Authorization` header, defined in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.2) +/// +/// The "Authorization" header field allows a user agent to authenticate +/// itself with an origin server -- usually, but not necessarily, after +/// receiving a 401 (Unauthorized) response. Its value consists of +/// 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). +/// +/// # 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}; +/// +/// fn handler(req: HttpRequest) -> Result { +/// let auth = Authorization::::parse(&req)?; +/// +/// Ok(format!("Hello, {}!", auth.username)) +/// } +/// ``` +pub struct Authorization(S); + +impl Authorization { + pub fn into_inner(self) -> S { + self.0 + } +} + +impl Header for Authorization { + #[inline] + fn name() -> HeaderName { + AUTHORIZATION + } + + fn parse(msg: &T) -> Result { + let header = msg.headers().get(AUTHORIZATION).ok_or(ParseError::Header)?; + let scheme = S::parse(header).map_err(|_| ParseError::Header)?; + + Ok(Authorization(scheme)) + } +} + +impl IntoHeaderValue for Authorization { + type Error = ::Error; + + fn try_into(self) -> Result::Error> { + self.0.try_into() + } +} + +impl fmt::Display for Authorization { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + 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 new file mode 100644 index 000000000..d1420a5dc --- /dev/null +++ b/src/headers/authorization/mod.rs @@ -0,0 +1,9 @@ +mod scheme; +mod header; +mod errors; + +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; diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs new file mode 100644 index 000000000..449ea2ad6 --- /dev/null +++ b/src/headers/authorization/scheme/basic.rs @@ -0,0 +1,191 @@ +use std::str; +use std::fmt; + +use base64; +use bytes::{BufMut, BytesMut}; +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; + +use headers::authorization::Scheme; +use headers::authorization::errors::ParseError; + +/// 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, +} + +impl Scheme for Basic { + fn parse(header: &HeaderValue) -> Result { + // "Basic *" length + if header.len() < 7 { + return Err(ParseError::Invalid); + } + + let mut parts = header.to_str()?.splitn(2, ' '); + match parts.next() { + Some(scheme) if scheme == "Basic" => (), + _ => return Err(ParseError::MissingScheme), + } + + let decoded = base64::decode(parts.next().ok_or(ParseError::Invalid)?)?; + 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() + .ok_or(ParseError::MissingField("password")) + .map(|password| { + if password.is_empty() { + None + } else { + Some(password.to_string()) + } + })?; + + Ok(Basic{ + username, + password, + }) + } +} + +impl fmt::Debug for Basic { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_fmt(format_args!("Basic {}:******", self.username)) + } +} + +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)) + } +} + +impl IntoHeaderValue for Basic { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result::Error> { + let mut credentials = BytesMut::with_capacity( + self.username.len() + self.password.as_ref().map_or(0, |pwd| pwd.len()) + ); + credentials.put(&self.username); + credentials.put_u8(b':'); + if let Some(ref password) = self.password { + credentials.put(password); + } + + // 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 "); + value.put(&encoded); + + HeaderValue::from_shared(value.freeze()) + } +} + +#[cfg(test)] +mod tests { + use actix_web::http::header::{HeaderValue, IntoHeaderValue}; + use super::{Scheme, Basic}; + + #[test] + fn test_parse_header() { + let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + let scheme = Basic::parse(&value); + + assert!(scheme.is_ok()); + let scheme = scheme.unwrap(); + assert_eq!(scheme.username, "Aladdin"); + assert_eq!(scheme.password, Some("open sesame".to_string())); + } + + #[test] + fn test_empty_password() { + let value = HeaderValue::from_static("Basic QWxhZGRpbjo="); + let scheme = Basic::parse(&value); + + assert!(scheme.is_ok()); + let scheme = scheme.unwrap(); + assert_eq!(scheme.username, "Aladdin"); + assert_eq!(scheme.password, None); + } + + #[test] + fn test_empty_header() { + let value = HeaderValue::from_static(""); + let scheme = Basic::parse(&value); + + assert!(scheme.is_err()); + } + + #[test] + fn test_wrong_scheme() { + let value = HeaderValue::from_static("THOUSHALLNOTPASS please?"); + let scheme = Basic::parse(&value); + + assert!(scheme.is_err()); + } + + #[test] + fn test_missing_credentials() { + let value = HeaderValue::from_static("Basic "); + let scheme = Basic::parse(&value); + + assert!(scheme.is_err()); + } + + #[test] + fn test_missing_credentials_colon() { + let value = HeaderValue::from_static("Basic QWxsYWRpbg=="); + let scheme = Basic::parse(&value); + + assert!(scheme.is_err()); + } + + #[test] + fn test_into_header_value() { + let basic = Basic { + username: "Aladdin".to_string(), + password: Some("open sesame".to_string()), + }; + + let result = basic.try_into(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")); + } +} + + +#[cfg(all(test, feature = "nightly"))] +mod benches { + use test::Bencher; + + use actix_web::http::header::{HeaderValue, IntoHeaderValue}; + + use super::{Basic, Scheme}; + + #[bench] + fn bench_parsing(b: &mut Bencher) { + let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + 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()), + }; + + basic.try_into() + }) + } +} diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs new file mode 100644 index 000000000..2a7257cd9 --- /dev/null +++ b/src/headers/authorization/scheme/bearer.rs @@ -0,0 +1,111 @@ +use std::fmt; + +use bytes::{BufMut, BytesMut}; +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; + +use headers::authorization::scheme::Scheme; +use headers::authorization::errors::ParseError; + +/// 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, +} + +impl Scheme for Bearer { + fn parse(header: &HeaderValue) -> Result { + // "Bearer *" length + if header.len() < 8 { + return Err(ParseError::Invalid); + } + + let mut parts = header.to_str()?.splitn(2, ' '); + match parts.next() { + Some(scheme) if scheme == "Bearer" => (), + _ => return Err(ParseError::MissingScheme), + } + + let token = parts.next().ok_or(ParseError::Invalid)?; + + Ok(Bearer{ + token: token.to_string(), + }) + } +} + +impl fmt::Debug for Bearer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_fmt(format_args!("Bearer ******")) + } +} + +impl fmt::Display for Bearer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_fmt(format_args!("Bearer {}", self.token)) + } +} + +impl IntoHeaderValue for Bearer { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result::Error> { + let mut buffer = BytesMut::with_capacity(7 + self.token.len()); + buffer.put("Bearer "); + buffer.put(self.token); + + HeaderValue::from_shared(buffer.freeze()) + } +} + +#[cfg(test)] +mod tests { + use actix_web::http::header::{HeaderValue, IntoHeaderValue}; + use super::{Scheme, Bearer}; + + #[test] + fn test_parse_header() { + let value = HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM"); + let scheme = Bearer::parse(&value); + + assert!(scheme.is_ok()); + let scheme = scheme.unwrap(); + assert_eq!(scheme.token, "mF_9.B5f-4.1JqM"); + } + + #[test] + fn test_empty_header() { + let value = HeaderValue::from_static(""); + let scheme = Bearer::parse(&value); + + assert!(scheme.is_err()); + } + + #[test] + fn test_wrong_scheme() { + let value = HeaderValue::from_static("OAuthToken foo"); + let scheme = Bearer::parse(&value); + + assert!(scheme.is_err()); + } + + #[test] + fn test_missing_token() { + let value = HeaderValue::from_static("Bearer "); + let scheme = Bearer::parse(&value); + + assert!(scheme.is_err()); + } + + #[test] + fn test_into_header_value() { + let bearer = Bearer { + token: "mF_9.B5f-4.1JqM".to_string(), + }; + + let result = bearer.try_into(); + assert!(result.is_ok()); + 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 new file mode 100644 index 000000000..42524129e --- /dev/null +++ b/src/headers/authorization/scheme/mod.rs @@ -0,0 +1,13 @@ +use std::fmt::{Debug, Display}; + +use actix_web::http::header::{IntoHeaderValue, HeaderValue}; + +pub mod basic; +pub mod bearer; + +use headers::authorization::errors::ParseError; + +/// Authentication scheme for [`Authorization`](./struct.Authorization.html) header. +pub trait Scheme: IntoHeaderValue + Debug + Display + Clone + Send + Sync { + fn parse(header: &HeaderValue) -> Result; +} diff --git a/src/headers/mod.rs b/src/headers/mod.rs new file mode 100644 index 000000000..0d0d12342 --- /dev/null +++ b/src/headers/mod.rs @@ -0,0 +1,2 @@ +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 new file mode 100644 index 000000000..492b2677c --- /dev/null +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -0,0 +1,91 @@ +use std::str; +use std::fmt; +use std::default::Default; + +use bytes::{BufMut, Bytes, BytesMut}; +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; + +use super::Challenge; + +/// Challenge for `WWW-Authenticate` header with HTTP Basic auth scheme, +/// described in [RFC 7617](https://tools.ietf.org/html/rfc7617) +#[derive(Debug, Clone)] +pub struct Basic { + // "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A + pub realm: Option, +} + +impl Challenge for Basic { + fn to_bytes(&self) -> Bytes { + // 5 is for `"Basic"`, 9 is for `"realm=\"\""` + let length = 5 + self.realm.as_ref().map_or(0, |realm| realm.len() + 9); + let mut buffer = BytesMut::with_capacity(length); + buffer.put("Basic"); + if let Some(ref realm) = self.realm { + buffer.put(" realm=\""); + buffer.put(realm); + buffer.put("\""); + } + + buffer.freeze() + } +} + +impl fmt::Display for Basic { + 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 Basic { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result::Error> { + HeaderValue::from_shared(self.to_bytes()) + } +} + + +impl Default for Basic { + fn default() -> Self { + Self { + realm: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::Basic; + use actix_web::http::header::IntoHeaderValue; + + #[test] + fn test_plain_into_header_value() { + let challenge = Basic { + realm: None, + }; + + let value = challenge.try_into(); + assert!(value.is_ok()); + let value = value.unwrap(); + assert_eq!(value, "Basic"); + } + + #[test] + fn test_with_realm_into_header_value() { + let challenge = Basic { + realm: Some("Restricted area".to_string()), + }; + + let value = challenge.try_into(); + assert!(value.is_ok()); + let value = value.unwrap(); + assert_eq!(value, "Basic realm=\"Restricted area\""); + } +} diff --git a/src/headers/www_authenticate/challenge/bearer.rs b/src/headers/www_authenticate/challenge/bearer.rs new file mode 100644 index 000000000..c3885e1aa --- /dev/null +++ b/src/headers/www_authenticate/challenge/bearer.rs @@ -0,0 +1,136 @@ +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 capacity = 6 + + self.realm.as_ref().map_or(0, |realm| realm.len() + 9) + + self.scope.as_ref().map_or(0, |scope| scope.len() + 9) + + 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 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 = error_repr.len() + 9; // 9 is for `" error=\"\""` + if remaining < required { + buffer.reserve(required - remaining); + } + 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()) + } +} diff --git a/src/headers/www_authenticate/challenge/mod.rs b/src/headers/www_authenticate/challenge/mod.rs new file mode 100644 index 000000000..372de9884 --- /dev/null +++ b/src/headers/www_authenticate/challenge/mod.rs @@ -0,0 +1,12 @@ +use std::fmt::{Debug, Display}; + +use bytes::Bytes; +use actix_web::http::header::IntoHeaderValue; + +pub mod basic; +pub mod bearer; + +/// Authentication challenge for `WWW-Authenticate` header. +pub trait Challenge: IntoHeaderValue + Debug + Display + Clone + Send + Sync { + fn to_bytes(&self) -> Bytes; +} diff --git a/src/headers/www_authenticate/header.rs b/src/headers/www_authenticate/header.rs new file mode 100644 index 000000000..d439ea97b --- /dev/null +++ b/src/headers/www_authenticate/header.rs @@ -0,0 +1,49 @@ +use actix_web::{HttpMessage}; +use actix_web::error::ParseError; +use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE}; + +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); + +impl Header for WWWAuthenticate { + fn name() -> HeaderName { + WWW_AUTHENTICATE + } + + fn parse(_msg: &T) -> Result { + unimplemented!() + } +} + +impl IntoHeaderValue for WWWAuthenticate { + type Error = ::Error; + + fn try_into(self) -> Result::Error> { + self.0.try_into() + } +} diff --git a/src/headers/www_authenticate/mod.rs b/src/headers/www_authenticate/mod.rs new file mode 100644 index 000000000..3c874a48a --- /dev/null +++ b/src/headers/www_authenticate/mod.rs @@ -0,0 +1,7 @@ +mod challenge; +mod header; + +pub use self::header::WWWAuthenticate; +pub use self::challenge::Challenge; +pub use self::challenge::basic; +pub use self::challenge::bearer; diff --git a/src/lib.rs b/src/lib.rs index bcd182dcd..f97de92c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,15 @@ -//! HTTP authorization routines for [actix-web](https://github.com/actix/actix-web) framework. +//! HTTP Authorization support for [actix-web](https://actix.rs) framework. //! -//! Currently supported schemas: -//! * Basic ([RFC-7617](https://tools.ietf.org/html/rfc7617)) +//! 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. + +#![cfg_attr(feature = "nightly", feature(test))] +#[cfg(feature = "nightly")] extern crate test; -extern crate bytes; -extern crate percent_encoding; extern crate actix_web; +extern crate bytes; extern crate base64; -mod errors; -pub mod basic; +pub mod headers; +pub mod extractors;