diff --git a/actix-web-httpauth/src/extractors/api_key.rs b/actix-web-httpauth/src/extractors/api_key.rs new file mode 100644 index 0000000000..ec03b12848 --- /dev/null +++ b/actix-web-httpauth/src/extractors/api_key.rs @@ -0,0 +1,113 @@ +//! Extractor for the "Basic" HTTP Authentication Scheme. + +use std::borrow::Cow; + +use actix_utils::future::{ready, Ready}; +use actix_web::{dev::Payload, http::header::Header, FromRequest, HttpRequest}; + +use super::{config::AuthExtractorConfig, errors::AuthenticationError}; +use crate::headers::{ + api_key::{APIKey, XAPIKey}, + www_authenticate::basic::Basic as Challenge, +}; + +/// [`BasicAuth`] extractor configuration used for [`WWW-Authenticate`] header later. +/// +/// [`WWW-Authenticate`]: crate::headers::www_authenticate::WwwAuthenticate +#[derive(Debug, Clone, Default)] +pub struct Config(Challenge); + +impl Config { + /// Set challenge `realm` attribute. + /// + /// The "realm" attribute indicates the scope of protection in the manner described in HTTP/1.1 + /// [RFC 2617 §1.2](https://tools.ietf.org/html/rfc2617#section-1.2). + 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 AuthExtractorConfig for Config { + type Inner = Challenge; + + fn into_inner(self) -> Self::Inner { + self.0 + } +} + +/// Extractor for HTTP Basic auth. +/// +/// # Examples +/// ``` +/// use actix_web_httpauth::extractors::basic::BasicAuth; +/// +/// async 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. +/// +/// # Examples +/// ``` +/// use actix_web::{web, App}; +/// use actix_web_httpauth::extractors::basic::{self, BasicAuth}; +/// +/// async fn index(auth: BasicAuth) -> String { +/// format!("Hello, {}!", auth.user_id()) +/// } +/// +/// App::new() +/// .app_data(basic::Config::default().realm("Restricted area")) +/// .service(web::resource("/index.html").route(web::get().to(index))); +/// ``` +/// +/// [app data]: https://docs.rs/actix-web/4/actix_web/struct.App.html#method.app_data +#[derive(Debug, Clone)] +pub struct APIKeyAuth(APIKey); + +impl APIKeyAuth { + /// Returns client's user-ID. + pub fn api_key(&self) -> &str { + self.0.api_key() + } +} + +impl From for APIKeyAuth { + fn from(api_key: APIKey) -> Self { + Self(api_key) + } +} + +impl FromRequest for APIKeyAuth { + type Future = Ready>; + type Error = AuthenticationError; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> ::Future { + ready( + XAPIKey::::parse(req) + .map(|auth| APIKeyAuth(auth.into_scheme())) + .map_err(|err| { + log::debug!("`APIKeAuth` extract error: {}", err); + + let challenge = req + .app_data::() + .map(|config| config.0.clone()) + .unwrap_or_default(); + + AuthenticationError::new(challenge) + }), + ) + } +} diff --git a/actix-web-httpauth/src/extractors/mod.rs b/actix-web-httpauth/src/extractors/mod.rs index bd652b1f7f..05cdcc255b 100644 --- a/actix-web-httpauth/src/extractors/mod.rs +++ b/actix-web-httpauth/src/extractors/mod.rs @@ -1,5 +1,6 @@ //! Type-safe authentication information extractors. +pub mod api_key; pub mod basic; pub mod bearer; mod config; diff --git a/actix-web-httpauth/src/headers/api_key/header.rs b/actix-web-httpauth/src/headers/api_key/header.rs new file mode 100644 index 0000000000..5014becc90 --- /dev/null +++ b/actix-web-httpauth/src/headers/api_key/header.rs @@ -0,0 +1,84 @@ +use std::fmt; + +use actix_web::{ + error::ParseError, + http::header::{Header, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION}, + HttpMessage, +}; + +use crate::headers::api_key::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` is generic over an [authentication scheme](Scheme). +/// +/// # Examples +/// ``` +/// # use actix_web::{HttpRequest, Result, 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.as_ref().user_id())) +/// } +/// ``` +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct XAPIKey(S); + +impl XAPIKey { + /// Consumes `X-API-KEY` header and returns inner [`Scheme`] implementation. + pub fn into_scheme(self) -> S { + self.0 + } +} + +impl From for XAPIKey { + fn from(scheme: S) -> XAPIKey { + XAPIKey(scheme) + } +} + +impl AsRef for XAPIKey { + fn as_ref(&self) -> &S { + &self.0 + } +} + +impl AsMut for XAPIKey { + fn as_mut(&mut self) -> &mut S { + &mut self.0 + } +} + +impl fmt::Display for XAPIKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Header for XAPIKey { + #[inline] + fn name() -> HeaderName { + HeaderName::from_static("x-api-key") + } + + fn parse(msg: &T) -> Result { + let header = msg.headers().get(Self::name()).ok_or(ParseError::Header)?; + let scheme = S::parse(header).map_err(|_| ParseError::Header)?; + + Ok(XAPIKey(scheme)) + } +} + +impl TryIntoHeaderValue for XAPIKey { + type Error = ::Error; + + fn try_into_value(self) -> Result { + self.0.try_into_value() + } +} diff --git a/actix-web-httpauth/src/headers/api_key/mod.rs b/actix-web-httpauth/src/headers/api_key/mod.rs new file mode 100644 index 0000000000..8fcecc6eda --- /dev/null +++ b/actix-web-httpauth/src/headers/api_key/mod.rs @@ -0,0 +1,9 @@ +//! `Authorization` header and various auth schemes. + +mod header; +mod scheme; + +pub use self::{ + header::XAPIKey, + scheme::{api_key::APIKey, Scheme}, +}; diff --git a/actix-web-httpauth/src/headers/api_key/scheme/api_key.rs b/actix-web-httpauth/src/headers/api_key/scheme/api_key.rs new file mode 100644 index 0000000000..07f93651e5 --- /dev/null +++ b/actix-web-httpauth/src/headers/api_key/scheme/api_key.rs @@ -0,0 +1,113 @@ +use std::{borrow::Cow, fmt, str}; + +use actix_web::http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue}; + +use crate::headers::api_key::Scheme; +use crate::headers::errors::ParseError; + +/// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617) +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct APIKey { + api_key: Cow<'static, str>, +} + +impl APIKey { + /// Creates `Basic` credentials with provided `user_id` and optional + /// `password`. + /// + /// # Examples + /// ``` + /// # use actix_web_httpauth::headers::authorization::Basic; + /// let credentials = Basic::new("Alladin", Some("open sesame")); + /// ``` + pub fn new(api_key: U) -> APIKey + where + U: Into>, + { + APIKey { + api_key: api_key.into(), + } + } + + /// Returns client's user-ID. + pub fn api_key(&self) -> &str { + &self.api_key.as_ref() + } +} + +impl Scheme for APIKey { + fn parse(header: &HeaderValue) -> Result { + // "Basic *" length + if header.len() < 36 { + return Err(ParseError::Invalid); + } + let api_key = header.to_str()?.to_string(); + + Ok(APIKey::new(api_key)) + } +} + +impl fmt::Debug for APIKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("APIKey: ******")) + } +} + +impl fmt::Display for APIKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("APIKey: ******")) + } +} + +impl TryIntoHeaderValue for APIKey { + type Error = InvalidHeaderValue; + + fn try_into_value(self) -> Result { + let value = String::from(self.api_key); + HeaderValue::from_maybe_shared(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_header() { + let key = "0451f2f1-74a7-4b8c-994d-2f67675ba07c"; + let value = HeaderValue::from_static(key); + let scheme = APIKey::parse(&value); + + assert!(scheme.is_ok()); + let scheme = scheme.unwrap(); + assert_eq!(scheme.api_key, key); + } + + #[test] + fn test_empty_header() { + let value = HeaderValue::from_static(""); + let scheme = APIKey::parse(&value); + + assert!(scheme.is_err()); + } + + #[test] + fn test_wrong_scheme() { + let value = HeaderValue::from_static("THOUSHALLNOTPASS please?"); + let scheme = APIKey::parse(&value); + + assert!(scheme.is_err()); + } + + #[test] + fn test_into_header_value() { + let key = "0451f2f1-74a7-4b8c-994d-2f67675ba07c"; + let basic = APIKey { + api_key: key.into(), + }; + + let result = basic.try_into_value(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), HeaderValue::from_static(key)); + } +} diff --git a/actix-web-httpauth/src/headers/api_key/scheme/mod.rs b/actix-web-httpauth/src/headers/api_key/scheme/mod.rs new file mode 100644 index 0000000000..148140c545 --- /dev/null +++ b/actix-web-httpauth/src/headers/api_key/scheme/mod.rs @@ -0,0 +1,13 @@ +use std::fmt::{Debug, Display}; + +use actix_web::http::header::{HeaderValue, TryIntoHeaderValue}; + +pub mod api_key; + +use crate::headers::errors::ParseError; + +/// Authentication scheme for [`Authorization`](super::Authorization) header. +pub trait Scheme: TryIntoHeaderValue + Debug + Display + Clone + Send + Sync { + /// Try to parse an authentication scheme from the `Authorization` header. + fn parse(header: &HeaderValue) -> Result; +} diff --git a/actix-web-httpauth/src/headers/authorization/mod.rs b/actix-web-httpauth/src/headers/authorization/mod.rs index f5a9ab4bef..05b58f5ec9 100644 --- a/actix-web-httpauth/src/headers/authorization/mod.rs +++ b/actix-web-httpauth/src/headers/authorization/mod.rs @@ -1,11 +1,9 @@ //! `Authorization` header and various auth schemes. -mod errors; mod header; mod scheme; pub use self::{ - errors::ParseError, header::Authorization, scheme::{basic::Basic, bearer::Bearer, Scheme}, }; diff --git a/actix-web-httpauth/src/headers/authorization/scheme/basic.rs b/actix-web-httpauth/src/headers/authorization/scheme/basic.rs index 38e511adb1..442409806f 100644 --- a/actix-web-httpauth/src/headers/authorization/scheme/basic.rs +++ b/actix-web-httpauth/src/headers/authorization/scheme/basic.rs @@ -6,7 +6,8 @@ use actix_web::{ }; use base64::{prelude::BASE64_STANDARD, Engine}; -use crate::headers::authorization::{errors::ParseError, Scheme}; +use crate::headers::authorization::Scheme; +use crate::headers::errors::ParseError; /// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617) #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] diff --git a/actix-web-httpauth/src/headers/authorization/scheme/bearer.rs b/actix-web-httpauth/src/headers/authorization/scheme/bearer.rs index 842c9be760..6b57984760 100644 --- a/actix-web-httpauth/src/headers/authorization/scheme/bearer.rs +++ b/actix-web-httpauth/src/headers/authorization/scheme/bearer.rs @@ -5,7 +5,8 @@ use actix_web::{ web::{BufMut, BytesMut}, }; -use crate::headers::authorization::{errors::ParseError, scheme::Scheme}; +use crate::headers::authorization::scheme::Scheme; +use crate::headers::errors::ParseError; /// Credentials for `Bearer` authentication scheme, defined in [RFC 6750]. /// diff --git a/actix-web-httpauth/src/headers/authorization/scheme/mod.rs b/actix-web-httpauth/src/headers/authorization/scheme/mod.rs index 63a8ed64d8..bc8f9a62fc 100644 --- a/actix-web-httpauth/src/headers/authorization/scheme/mod.rs +++ b/actix-web-httpauth/src/headers/authorization/scheme/mod.rs @@ -5,7 +5,7 @@ use actix_web::http::header::{HeaderValue, TryIntoHeaderValue}; pub mod basic; pub mod bearer; -use crate::headers::authorization::errors::ParseError; +use crate::headers::errors::ParseError; /// Authentication scheme for [`Authorization`](super::Authorization) header. pub trait Scheme: TryIntoHeaderValue + Debug + Display + Clone + Send + Sync { diff --git a/actix-web-httpauth/src/headers/authorization/errors.rs b/actix-web-httpauth/src/headers/errors.rs similarity index 100% rename from actix-web-httpauth/src/headers/authorization/errors.rs rename to actix-web-httpauth/src/headers/errors.rs diff --git a/actix-web-httpauth/src/headers/mod.rs b/actix-web-httpauth/src/headers/mod.rs index bf446efd1b..08afe190a6 100644 --- a/actix-web-httpauth/src/headers/mod.rs +++ b/actix-web-httpauth/src/headers/mod.rs @@ -1,4 +1,7 @@ //! Typed HTTP headers. +pub mod api_key; pub mod authorization; +/// +pub mod errors; pub mod www_authenticate; diff --git a/actix-web-httpauth/src/middleware.rs b/actix-web-httpauth/src/middleware.rs index 4fd96a735b..f9e91cb914 100644 --- a/actix-web-httpauth/src/middleware.rs +++ b/actix-web-httpauth/src/middleware.rs @@ -17,7 +17,7 @@ use actix_web::{ use futures_core::ready; use futures_util::future::{self, LocalBoxFuture, TryFutureExt as _}; -use crate::extractors::{basic, bearer}; +use crate::extractors::{api_key, basic, bearer}; /// Middleware for checking HTTP authentication. /// @@ -117,6 +117,35 @@ where } } +impl HttpAuthentication +where + F: Fn(ServiceRequest, api_key::APIKeyAuth) -> O, + O: Future>, +{ + /// Construct `HttpAuthentication` middleware for the HTTP "Basic" authentication scheme. + /// + /// # Examples + /// ``` + /// # use actix_web::{Error, dev::ServiceRequest}; + /// # use actix_web_httpauth::{extractors::basic::BasicAuth, middleware::HttpAuthentication}; + /// // In this example validator returns immediately, but since it is required to return + /// // anything that implements `IntoFuture` trait, it can be extended to query database or to + /// // do something else in a async manner. + /// async fn validator( + /// req: ServiceRequest, + /// credentials: BasicAuth, + /// ) -> Result { + /// // All users are great and more than welcome! + /// Ok(req) + /// } + /// + /// let middleware = HttpAuthentication::basic(validator); + /// ``` + pub fn api_key(process_fn: F) -> Self { + Self::with_fn(process_fn) + } +} + impl Transform for HttpAuthentication where S: Service, Error = Error> + 'static,