1
0
mirror of https://github.com/actix/actix-extras.git synced 2025-01-22 14:55:56 +01:00

Experimental middleware for HTTP auth -- alpha.2 version

This commit is contained in:
svartalf 2019-05-17 17:28:57 +03:00
parent 1c1e7f672b
commit cac6894ddd
20 changed files with 497 additions and 196 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "actix-web-httpauth"
version = "0.3.0-alpha.1"
version = "0.3.0-alpha.2"
authors = ["svartalf <self@svartalf.info>"]
description = "HTTP authentication schemes for actix-web"
readme = "README.md"
@ -15,13 +15,11 @@ edition = "2018"
[dependencies]
actix-web = { version = "1.0.0-beta.5", default_features = false }
futures = "0.1"
actix-service = "0.4.0"
bytes = "0.4"
base64 = "0.10"
[dev-dependencies]
futures = "0.1.27"
actix-service = "0.4.0"
[features]
default = []
nightly = []

View File

@ -1,89 +0,0 @@
use std::borrow::Cow;
use std::io;
use actix_service::{Service, Transform};
use actix_web::{dev, web, App, Error, HttpRequest, HttpServer};
use futures::future::{self, Either, FutureResult};
use futures::Poll;
use actix_web_httpauth::extractors::basic::{BasicAuth, Config};
use actix_web_httpauth::extractors::AuthenticationError;
struct Auth(Config);
impl<S, B> Transform<S> for Auth
where
S: Service<Request = dev::ServiceRequest, Response = dev::ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Request = dev::ServiceRequest;
type Response = dev::ServiceResponse<B>;
type Error = Error;
type Transform = AuthMiddleware<S>;
type InitError = ();
type Future = FutureResult<Self::Transform, Self::InitError>;
fn new_transform(&self, service: S) -> Self::Future {
future::ok(AuthMiddleware {
service,
auth: self.0.clone(),
})
}
}
struct AuthMiddleware<S> {
service: S,
auth: Config,
}
impl<S> AuthMiddleware<S> {
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<S, B> Service for AuthMiddleware<S>
where
S: Service<Request = dev::ServiceRequest, Response = dev::ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Request = dev::ServiceRequest;
type Response = dev::ServiceResponse<B>;
type Error = Error;
type Future = Either<S::Future, FutureResult<Self::Response, Self::Error>>;
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()
}

View File

@ -2,8 +2,8 @@ unstable_features = true
edition = "2018"
version = "Two"
wrap_comments = true
comment_width = 120
max_width = 120
comment_width = 80
max_width = 80
merge_imports = false
newline_style = "Unix"
struct_lit_single_line = false

View File

@ -6,8 +6,9 @@ use actix_web::dev::{Payload, ServiceRequest};
use actix_web::http::header::Header;
use actix_web::{FromRequest, HttpRequest};
use super::config::ExtractorConfig;
use super::config::AuthExtractorConfig;
use super::errors::AuthenticationError;
use super::AuthExtractor;
use crate::headers::authorization::{Authorization, Basic};
use crate::headers::www_authenticate::basic::Basic as Challenge;
@ -15,15 +16,16 @@ use crate::headers::www_authenticate::basic::Basic as Challenge;
/// used for [`WWW-Authenticate`] header later.
///
/// [`BasicAuth`]: ./struct.BasicAuth.html
/// [`WWW-Authenticate`]: ../../headers/www_authenticate/struct.WwwAuthenticate.html
/// [`WWW-Authenticate`]:
/// ../../headers/www_authenticate/struct.WwwAuthenticate.html
#[derive(Debug, Clone, Default)]
pub struct Config(Challenge);
impl Config {
/// Set challenge `realm` attribute.
///
/// The "realm" attribute indicates the scope of protection in the manner described in HTTP/1.1
/// [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2).
/// The "realm" attribute indicates the scope of protection in the manner
/// described in HTTP/1.1 [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2).
pub fn realm<T>(mut self, value: T) -> Config
where
T: Into<Cow<'static, str>>,
@ -39,7 +41,7 @@ impl AsRef<Challenge> for Config {
}
}
impl ExtractorConfig for Config {
impl AuthExtractorConfig for Config {
type Inner = Challenge;
fn into_inner(self) -> Self::Inner {
@ -61,7 +63,8 @@ impl ExtractorConfig for Config {
/// ```
///
/// If authentication fails, this extractor fetches the [`Config`] instance
/// from the [app data] in order to properly form the `WWW-Authenticate` response header.
/// from the [app data] in order to properly form the `WWW-Authenticate`
/// response header.
///
/// ## Example
///
@ -95,27 +98,6 @@ impl BasicAuth {
pub fn password(&self) -> Option<&Cow<'static, str>> {
self.0.password()
}
/// Try to parse actix-web' `ServiceRequest` and fetch the `BasicAuth` from it.
///
/// ## Warning
///
/// This function is used right now for middleware creation only
/// and might change or be totally removed,
/// depends on `actix-web = "1.0"` release changes.
///
/// This issue will be resolved in the `0.3.0` release.
/// Before that -- brace yourselves!
pub fn from_service_request(req: &mut ServiceRequest, config: &Config) -> <Self as FromRequest>::Future {
Authorization::<Basic>::parse(&req)
.map(|auth| BasicAuth(auth.into_scheme()))
.map_err(|_| {
// TODO: debug! the original error
let challenge = config.clone().into_inner();
AuthenticationError::new(challenge)
})
}
}
impl FromRequest for BasicAuth {
@ -123,13 +105,36 @@ impl FromRequest for BasicAuth {
type Config = Config;
type Error = AuthenticationError<Challenge>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> <Self as FromRequest>::Future {
fn from_request(
req: &HttpRequest,
_: &mut Payload,
) -> <Self as FromRequest>::Future {
Authorization::<Basic>::parse(req)
.map(|auth| BasicAuth(auth.into_scheme()))
.map_err(|_| {
// TODO: debug! the original error
let challenge = req
.get_app_data::<Self::Config>()
.app_data::<Self::Config>()
.map(|config| config.0.clone())
// TODO: Add trace! about `Default::default` call
.unwrap_or_else(Default::default);
AuthenticationError::new(challenge)
})
}
}
impl AuthExtractor for BasicAuth {
type Error = AuthenticationError<Challenge>;
type Future = Result<Self, Self::Error>;
fn from_service_request(req: &ServiceRequest) -> Self::Future {
Authorization::<Basic>::parse(req)
.map(|auth| BasicAuth(auth.into_scheme()))
.map_err(|_| {
// TODO: debug! the original error
let challenge = req
.app_data::<Config>()
.map(|config| config.0.clone())
// TODO: Add trace! about `Default::default` call
.unwrap_or_else(Default::default);

View File

@ -7,8 +7,9 @@ use actix_web::dev::{Payload, ServiceRequest};
use actix_web::http::header::Header;
use actix_web::{FromRequest, HttpRequest};
use super::config::ExtractorConfig;
use super::config::AuthExtractorConfig;
use super::errors::AuthenticationError;
use super::AuthExtractor;
use crate::headers::authorization;
use crate::headers::www_authenticate::bearer;
pub use crate::headers::www_authenticate::bearer::Error;
@ -20,8 +21,9 @@ pub struct Config(bearer::Bearer);
impl Config {
/// Set challenge `scope` attribute.
///
/// The `"scope"` attribute is a space-delimited list of case-sensitive scope values
/// indicating the required scope of the access token for accessing the requested resource.
/// The `"scope"` attribute is a space-delimited list of case-sensitive
/// scope values indicating the required scope of the access token for
/// accessing the requested resource.
pub fn scope<T: Into<Cow<'static, str>>>(mut self, value: T) -> Config {
self.0.scope = Some(value.into());
self
@ -29,8 +31,8 @@ impl Config {
/// Set challenge `realm` attribute.
///
/// The "realm" attribute indicates the scope of protection in the manner described in HTTP/1.1
/// [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2).
/// The "realm" attribute indicates the scope of protection in the manner
/// described in HTTP/1.1 [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2).
pub fn realm<T: Into<Cow<'static, str>>>(mut self, value: T) -> Config {
self.0.realm = Some(value.into());
self
@ -43,7 +45,7 @@ impl AsRef<bearer::Bearer> for Config {
}
}
impl ExtractorConfig for Config {
impl AuthExtractorConfig for Config {
type Inner = bearer::Bearer;
fn into_inner(self) -> Self::Inner {
@ -64,7 +66,8 @@ impl ExtractorConfig for Config {
/// ```
///
/// If authentication fails, this extractor fetches the [`Config`] instance
/// from the [app data] in order to properly form the `WWW-Authenticate` response header.
/// from the [app data] in order to properly form the `WWW-Authenticate`
/// response header.
///
/// ## Example
///
@ -78,7 +81,11 @@ impl ExtractorConfig for Config {
///
/// fn main() {
/// let app = App::new()
/// .data(Config::default().realm("Restricted area").scope("email photo"))
/// .data(
/// Config::default()
/// .realm("Restricted area")
/// .scope("email photo"),
/// )
/// .service(web::resource("/index.html").route(web::get().to(index)));
/// }
/// ```
@ -90,27 +97,6 @@ impl BearerAuth {
pub fn token(&self) -> &str {
self.0.token()
}
/// Try to parse actix-web' `ServiceRequest` and fetch the `BasicAuth` from it.
///
/// ## Warning
///
/// This function is used right now for middleware creation only
/// and might change or be totally removed,
/// depends on `actix-web = "1.0"` release changes.
///
/// This issue will be resolved in the `0.3.0` release.
/// Before that -- brace yourselves!
pub fn from_service_request(req: &mut ServiceRequest, config: &Config) -> <Self as FromRequest>::Future {
authorization::Authorization::<authorization::Bearer>::parse(&req)
.map(|auth| BearerAuth(auth.into_scheme()))
.map_err(|_| {
// TODO: debug! the original error
let challenge = config.clone().into_inner();
AuthenticationError::new(challenge)
})
}
}
impl FromRequest for BearerAuth {
@ -118,7 +104,10 @@ impl FromRequest for BearerAuth {
type Future = Result<Self, Self::Error>;
type Error = AuthenticationError<bearer::Bearer>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> <Self as FromRequest>::Future {
fn from_request(
req: &HttpRequest,
_payload: &mut Payload,
) -> <Self as FromRequest>::Future {
authorization::Authorization::<authorization::Bearer>::parse(req)
.map(|auth| BearerAuth(auth.into_scheme()))
.map_err(|_| {
@ -132,11 +121,30 @@ impl FromRequest for BearerAuth {
}
}
impl AuthExtractor for BearerAuth {
type Future = Result<Self, Self::Error>;
type Error = AuthenticationError<bearer::Bearer>;
fn from_service_request(req: &ServiceRequest) -> Self::Future {
authorization::Authorization::<authorization::Bearer>::parse(req)
.map(|auth| BearerAuth(auth.into_scheme()))
.map_err(|_| {
let bearer = req
.app_data::<Config>()
.map(|config| config.0.clone())
.unwrap_or_else(Default::default);
AuthenticationError::new(bearer)
})
}
}
/// Extended error customization for HTTP `Bearer` auth.
impl AuthenticationError<bearer::Bearer> {
/// Attach `Error` to the current Authentication error.
///
/// Error status code will be changed to the one provided by the `kind` Error.
/// Error status code will be changed to the one provided by the `kind`
/// Error.
pub fn with_error(mut self, kind: Error) -> Self {
*self.status_code_mut() = kind.status_code();
self.challenge_mut().error = Some(kind);

View File

@ -1,15 +1,21 @@
use super::AuthenticationError;
use crate::headers::www_authenticate::Challenge;
pub trait ExtractorConfig {
/// Trait implemented for types that provides configuration
/// for the authentication [extractors].
///
/// [extractors]: ./trait.AuthExtractor.html
pub trait AuthExtractorConfig {
/// Associated challenge type.
type Inner: Challenge;
/// Convert the config instance into a HTTP challenge.
fn into_inner(self) -> Self::Inner;
}
impl<T> From<T> for AuthenticationError<<T as ExtractorConfig>::Inner>
impl<T> From<T> for AuthenticationError<<T as AuthExtractorConfig>::Inner>
where
T: ExtractorConfig,
T: AuthExtractorConfig,
{
fn from(config: T) -> Self {
AuthenticationError::new(config.into_inner())

View File

@ -1,8 +1,33 @@
//! Type-safe authentication information extractors
use actix_web::dev::ServiceRequest;
use actix_web::Error;
use futures::IntoFuture;
pub mod basic;
pub mod bearer;
mod config;
mod errors;
pub use self::config::AuthExtractorConfig;
pub use self::errors::AuthenticationError;
/// Trait implemented by types that can extract
/// HTTP authentication scheme credentials from the request.
///
/// It is very similar to actix' `FromRequest` trait,
/// except it operates with a `ServiceRequest` struct instead,
/// therefore it can be used in the middlewares.
///
/// You will not need it unless you want to implement your own
/// authentication scheme.
pub trait AuthExtractor: Sized {
/// The associated error which can be returned.
type Error: Into<Error>;
/// Future that resolves into extracted credentials type.
type Future: IntoFuture<Item = Self, Error = Self::Error>;
/// Parse the authentication credentials from the actix' `ServiceRequest`.
fn from_service_request(req: &ServiceRequest) -> Self::Future;
}

View File

@ -1,7 +1,9 @@
use std::fmt;
use actix_web::error::ParseError;
use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION};
use actix_web::http::header::{
Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION,
};
use actix_web::HttpMessage;
use crate::headers::authorization::scheme::Scheme;
@ -14,7 +16,8 @@ use crate::headers::authorization::scheme::Scheme;
/// credentials containing the authentication information of the user
/// agent for the realm of the resource being requested.
///
/// `Authorization` header is generic over [authentication scheme](./trait.Scheme.html).
/// `Authorization` header is generic over [authentication
/// scheme](./trait.Scheme.html).
///
/// # Example
///
@ -35,7 +38,8 @@ impl<S> Authorization<S>
where
S: Scheme,
{
/// Consumes `Authorization` header and returns inner [`Scheme`] implementation.
/// Consumes `Authorization` header and returns inner [`Scheme`]
/// implementation.
///
/// [`Scheme`]: ./trait.Scheme.html
pub fn into_scheme(self) -> S {
@ -77,7 +81,8 @@ impl<S: Scheme> Header for Authorization<S> {
}
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError> {
let header = msg.headers().get(AUTHORIZATION).ok_or(ParseError::Header)?;
let header =
msg.headers().get(AUTHORIZATION).ok_or(ParseError::Header)?;
let scheme = S::parse(header).map_err(|_| ParseError::Header)?;
Ok(Authorization(scheme))

View File

@ -2,7 +2,9 @@ use std::borrow::Cow;
use std::fmt;
use std::str;
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes};
use actix_web::http::header::{
HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes,
};
use base64;
use bytes::{BufMut, BytesMut};
@ -18,7 +20,8 @@ pub struct Basic {
}
impl Basic {
/// Creates `Basic` credentials with provided `user_id` and optional `password`.
/// Creates `Basic` credentials with provided `user_id` and optional
/// `password`.
///
/// ## Example
///
@ -102,15 +105,19 @@ impl IntoHeaderValue for Basic {
type Error = InvalidHeaderValueBytes;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
let mut credentials =
BytesMut::with_capacity(self.user_id.len() + 1 + self.password.as_ref().map_or(0, |pwd| pwd.len()));
let mut credentials = BytesMut::with_capacity(
self.user_id.len()
+ 1
+ self.password.as_ref().map_or(0, |pwd| pwd.len()),
);
utils::put_cow(&mut credentials, &self.user_id);
credentials.put_u8(b':');
if let Some(ref password) = self.password {
utils::put_cow(&mut credentials, password);
}
// TODO: It would be nice not to allocate new `String` here but write directly to `value`
// TODO: It would be nice not to allocate new `String` here but write
// directly to `value`
let encoded = base64::encode(&credentials);
let mut value = BytesMut::with_capacity(6 + encoded.len());
value.put("Basic ");
@ -127,7 +134,8 @@ mod tests {
#[test]
fn test_parse_header() {
let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
let value =
HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
let scheme = Basic::parse(&value);
assert!(scheme.is_ok());
@ -205,7 +213,8 @@ mod benches {
#[bench]
fn bench_parsing(b: &mut Bencher) {
let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
let value =
HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
b.iter(|| Basic::parse(&value));
}

View File

@ -1,7 +1,9 @@
use std::borrow::Cow;
use std::fmt;
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes};
use actix_web::http::header::{
HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes,
};
use bytes::{BufMut, BytesMut};
use crate::headers::authorization::errors::ParseError;
@ -10,7 +12,8 @@ use crate::utils;
/// Credentials for `Bearer` authentication scheme, defined in [RFC6750](https://tools.ietf.org/html/rfc6750)
///
/// Should be used in combination with [`Authorization`](./struct.Authorization.html) header.
/// Should be used in combination with
/// [`Authorization`](./struct.Authorization.html) header.
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Bearer {
token: Cow<'static, str>,
@ -130,6 +133,9 @@ mod tests {
let result = bearer.try_into();
assert!(result.is_ok());
assert_eq!(result.unwrap(), HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM"));
assert_eq!(
result.unwrap(),
HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM")
);
}
}

View File

@ -7,8 +7,11 @@ pub mod bearer;
use crate::headers::authorization::errors::ParseError;
/// Authentication scheme for [`Authorization`](./struct.Authorization.html) header.
pub trait Scheme: IntoHeaderValue + Debug + Display + Clone + Send + Sync {
/// Authentication scheme for [`Authorization`](./struct.Authorization.html)
/// header.
pub trait Scheme:
IntoHeaderValue + Debug + Display + Clone + Send + Sync
{
/// Try to parse the authentication scheme from the `Authorization` header.
fn parse(header: &HeaderValue) -> Result<Self, ParseError>;
}

View File

@ -5,7 +5,9 @@ use std::default::Default;
use std::fmt;
use std::str;
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes};
use actix_web::http::header::{
HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes,
};
use bytes::{BufMut, Bytes, BytesMut};
use super::Challenge;
@ -24,7 +26,9 @@ use crate::utils;
/// fn index(_req: HttpRequest) -> HttpResponse {
/// let challenge = Basic::with_realm("Restricted area");
///
/// HttpResponse::Unauthorized().set(WwwAuthenticate(challenge)).finish()
/// HttpResponse::Unauthorized()
/// .set(WwwAuthenticate(challenge))
/// .finish()
/// }
/// ```
///

View File

@ -2,7 +2,9 @@ use std::borrow::Cow;
use std::fmt;
use std::str;
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes};
use actix_web::http::header::{
HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes,
};
use bytes::{BufMut, Bytes, BytesMut};
use super::super::Challenge;
@ -16,7 +18,9 @@ use crate::utils;
///
/// ```rust
/// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
/// use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer, Error};
/// use actix_web_httpauth::headers::www_authenticate::bearer::{
/// Bearer, Error,
/// };
/// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate;
///
/// fn index(_req: HttpRequest) -> HttpResponse {
@ -28,7 +32,9 @@ use crate::utils;
/// .error_uri("http://example.org")
/// .finish();
///
/// HttpResponse::Unauthorized().set(WwwAuthenticate(challenge)).finish()
/// HttpResponse::Unauthorized()
/// .set(WwwAuthenticate(challenge))
/// .finish()
/// }
/// ```
///
@ -62,7 +68,10 @@ impl Bearer {
#[doc(hidden)]
impl Challenge for Bearer {
fn to_bytes(&self) -> Bytes {
let desc_uri_required = self.error_description.as_ref().map_or(0, |desc| desc.len() + 20)
let desc_uri_required = self
.error_description
.as_ref()
.map_or(0, |desc| desc.len() + 20)
+ self.error_uri.as_ref().map_or(0, |url| url.len() + 12);
let capacity = 6
+ self.realm.as_ref().map_or(0, |realm| realm.len() + 9)

View File

@ -5,15 +5,18 @@ use actix_web::http::StatusCode;
/// Bearer authorization error types, described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3.1)
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum Error {
/// The request is missing a required parameter, includes an unsupported parameter
/// or parameter value, repeats the same parameter, uses more than one method
/// for including an access token, or is otherwise malformed.
/// The request is missing a required parameter, includes an unsupported
/// parameter or parameter value, repeats the same parameter, uses more
/// than one method for including an access token, or is otherwise
/// malformed.
InvalidRequest,
/// The access token provided is expired, revoked, malformed, or invalid for other reasons.
/// The access token provided is expired, revoked, malformed, or invalid
/// for other reasons.
InvalidToken,
/// The request requires higher privileges than provided by the access token.
/// The request requires higher privileges than provided by the access
/// token.
InsufficientScope,
}

View File

@ -4,7 +4,9 @@ use super::*;
fn to_bytes() {
let b = Bearer::build()
.error(Error::InvalidToken)
.error_description("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found")
.error_description(
"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found",
)
.finish();
assert_eq!(

View File

@ -7,7 +7,9 @@ pub mod basic;
pub mod bearer;
/// Authentication challenge for `WWW-Authenticate` header.
pub trait Challenge: IntoHeaderValue + Debug + Display + Clone + Send + Sync {
pub trait Challenge:
IntoHeaderValue + Debug + Display + Clone + Send + Sync
{
/// Converts the challenge into a bytes suitable for HTTP transmission.
fn to_bytes(&self) -> Bytes;
}

View File

@ -1,5 +1,7 @@
use actix_web::error::ParseError;
use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE};
use actix_web::http::header::{
Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE,
};
use actix_web::HttpMessage;
use super::Challenge;
@ -7,8 +9,8 @@ use super::Challenge;
/// `WWW-Authenticate` header, described in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.1)
///
/// This header is generic over [Challenge](./trait.Challenge.html) trait,
/// see [Basic](./basic/struct.Basic.html) and [Bearer](./bearer/struct.Bearer.html)
/// challenges for details.
/// see [Basic](./basic/struct.Basic.html) and
/// [Bearer](./bearer/struct.Bearer.html) challenges for details.
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
pub struct WwwAuthenticate<C: Challenge>(pub C);

View File

@ -1,7 +1,9 @@
//! HTTP Authorization support for [actix-web](https://actix.rs) framework.
//!
//! Provides [Authorization] and [WWW-Authenticate] headers,
//! and [extractors] for an [Authorization] header.
//! Provides:
//! * typed [Authorization] and [WWW-Authenticate] headers
//! * [extractors] for an [Authorization] header
//! * [middleware] for easier authorization checking
//!
//! ## Supported schemes
//!
@ -11,11 +13,14 @@
//! [Authorization]: `crate::headers::authorization::Authorization`
//! [WWW-Authenticate]: `crate::headers::www_authenticate::WwwAuthenticate`
//! [extractors]: https://actix.rs/docs/extractors/
//! [middleware]: ./middleware/
#![forbid(bare_trait_objects)]
#![forbid(missing_docs)]
#![deny(bare_trait_objects)]
#![deny(missing_docs)]
#![deny(unused)]
#![cfg_attr(feature = "nightly", feature(test))]
pub mod extractors;
pub mod headers;
pub mod middleware;
mod utils;

297
src/middleware.rs Normal file
View File

@ -0,0 +1,297 @@
//! HTTP Authentication middleware.
use std::marker::PhantomData;
use std::rc::Rc;
use actix_service::{Service, Transform};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::Error;
use futures::future::{self, FutureResult};
use futures::{Async, Future, IntoFuture, Poll};
use crate::extractors::{basic, bearer, AuthExtractor};
/// Middleware for checking HTTP authentication.
///
/// If there is no `Authorization` header in the request,
/// this middleware returns an error immediately,
/// without calling the `F` callback.
/// Otherwise, it will pass parsed credentials into it.
pub struct HttpAuthentication<T, F>
where
T: AuthExtractor,
{
validator_fn: Rc<F>,
_extractor: PhantomData<T>,
}
impl<T, F, O> HttpAuthentication<T, F>
where
T: AuthExtractor,
F: FnMut(&mut ServiceRequest, T) -> O,
O: IntoFuture<Item = (), Error = Error>,
{
/// Construct `HttpAuthentication` middleware
/// with the provided auth extractor `T` and
/// validation callback `F`.
pub fn with_fn(validator_fn: F) -> HttpAuthentication<T, F> {
HttpAuthentication {
validator_fn: Rc::new(validator_fn),
_extractor: PhantomData,
}
}
}
impl<F, O> HttpAuthentication<basic::BasicAuth, F>
where
F: FnMut(&mut ServiceRequest, basic::BasicAuth) -> O,
O: IntoFuture<Item = (), Error = Error>,
{
/// Construct `HttpAuthentication` middleware for the HTTP "Basic"
/// authentication scheme.
///
/// ## Example
///
/// ```
/// # use actix_web::Error;
/// # use actix_web::dev::ServiceRequest;
/// # use futures::future::{self, FutureResult};
/// # use actix_web_httpauth::middleware::HttpAuthentication;
/// # use actix_web_httpauth::extractors::basic::BasicAuth;
/// // In this example validator returns immediately,
/// // but since it is required to return anything
/// // that implements `IntoFuture` trait,
/// // it can be extended to query database
/// // or to do something else in a async manner.
/// fn validator(
/// req: &mut ServiceRequest,
/// credentials: BasicAuth,
/// ) -> FutureResult<(), Error> {
/// // All users are great and more than welcome!
/// future::ok(())
/// }
///
/// let middleware = HttpAuthentication::basic(validator);
/// ```
pub fn basic(validator_fn: F) -> Self {
Self::with_fn(validator_fn)
}
}
impl<F, O> HttpAuthentication<bearer::BearerAuth, F>
where
F: FnMut(&mut ServiceRequest, bearer::BearerAuth) -> O,
O: IntoFuture<Item = (), Error = Error>,
{
/// Construct `HttpAuthentication` middleware for the HTTP "Bearer"
/// authentication scheme.
/// ## Example
///
/// ```
/// # use actix_web::Error;
/// # use actix_web::dev::ServiceRequest;
/// # use futures::future::{self, FutureResult};
/// # use actix_web_httpauth::middleware::HttpAuthentication;
/// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth};
/// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig};
/// fn validator(req: &mut ServiceRequest, credentials: BearerAuth) -> FutureResult<(), Error> {
/// if credentials.token() == "mF_9.B5f-4.1JqM" {
/// future::ok(())
/// } else {
/// let config = req.app_data::<Config>()
/// .map(|data| data.get_ref().clone())
/// .unwrap_or_else(Default::default)
/// .scope("urn:example:channel=HBO&urn:example:rating=G,PG-13");
///
/// future::err(AuthenticationError::from(config).into())
/// }
/// }
///
/// let middleware = HttpAuthentication::bearer(validator);
/// ```
pub fn bearer(validator_fn: F) -> Self {
Self::with_fn(validator_fn)
}
}
impl<S, B, T, F, O> Transform<S> for HttpAuthentication<T, F>
where
S: Service<
Request = ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
> + 'static,
S::Future: 'static,
F: Fn(&mut ServiceRequest, T) -> O + 'static,
O: IntoFuture<Item = (), Error = Error> + 'static,
T: AuthExtractor + 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = AuthenticationMiddleware<S, F, T>;
type InitError = ();
type Future = FutureResult<Self::Transform, Self::InitError>;
fn new_transform(&self, service: S) -> Self::Future {
future::ok(AuthenticationMiddleware {
service: Some(service),
validator_fn: self.validator_fn.clone(),
_extractor: PhantomData,
})
}
}
#[doc(hidden)]
pub struct AuthenticationMiddleware<S, F, T>
where
T: AuthExtractor,
{
service: Option<S>,
validator_fn: Rc<F>,
_extractor: PhantomData<T>,
}
impl<S, B, F, T, O> Service for AuthenticationMiddleware<S, F, T>
where
S: Service<
Request = ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
> + 'static,
S::Future: 'static,
F: Fn(&mut ServiceRequest, T) -> O + 'static,
O: IntoFuture<Item = (), Error = Error> + 'static,
T: AuthExtractor + 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = S::Error;
type Future = Box<dyn Future<Item = ServiceResponse<B>, Error = Error>>;
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
self.service
.as_mut()
.expect("AuthenticationMiddleware was called already")
.poll_ready()
}
fn call(&mut self, req: Self::Request) -> Self::Future {
let validator_fn = self.validator_fn.clone();
let mut service = self
.service
.take()
.expect("AuthenticationMiddleware was called twice");
let f = Extract::new(req)
.and_then(move |(req, credentials)| {
Validate::new(req, validator_fn, credentials)
})
.and_then(move |req| service.call(req));
Box::new(f)
}
}
struct Extract<T> {
req: Option<ServiceRequest>,
f: Option<Box<dyn Future<Item = T, Error = Error>>>,
_extractor: PhantomData<T>,
}
impl<T> Extract<T> {
pub fn new(req: ServiceRequest) -> Self {
Extract {
req: Some(req),
f: None,
_extractor: PhantomData,
}
}
}
impl<T> Future for Extract<T>
where
T: AuthExtractor,
T::Future: 'static,
T::Error: 'static,
{
type Item = (ServiceRequest, T);
type Error = Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
if self.f.is_none() {
let req =
self.req.as_ref().expect("Extract future was polled twice!");
let f = T::from_service_request(req)
.into_future()
.map_err(Into::into);
self.f = Some(Box::new(f));
}
let f = self
.f
.as_mut()
.expect("Extraction future should be initialized at this point");
let credentials = futures::try_ready!(f.poll());
let req = self.req.take().expect("Extract future was polled twice!");
Ok(Async::Ready((req, credentials)))
}
}
struct Validate<F, T> {
req: Option<ServiceRequest>,
validation_f: Option<Box<dyn Future<Item = (), Error = Error>>>,
validator_fn: Rc<F>,
credentials: Option<T>,
}
impl<F, T> Validate<F, T> {
pub fn new(
req: ServiceRequest,
validator_fn: Rc<F>,
credentials: T,
) -> Self {
Validate {
req: Some(req),
credentials: Some(credentials),
validator_fn,
validation_f: None,
}
}
}
impl<F, T, O> Future for Validate<F, T>
where
F: Fn(&mut ServiceRequest, T) -> O,
O: IntoFuture<Item = (), Error = Error> + 'static,
{
type Item = ServiceRequest;
type Error = Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
if self.validation_f.is_none() {
let req = self
.req
.as_mut()
.expect("Unable to get the mutable access to the request");
let credentials = self
.credentials
.take()
.expect("Validate future was polled in some weird manner");
let f = (self.validator_fn)(req, credentials).into_future();
self.validation_f = Some(Box::new(f));
}
let f = self
.validation_f
.as_mut()
.expect("Validation future should exist at this moment");
// We do not care about returned `Ok(())`
futures::try_ready!(f.poll());
let req = self.req.take().expect("Validate future was polled already");
Ok(Async::Ready(req))
}
}

View File

@ -2,7 +2,8 @@ use std::borrow::Cow;
use bytes::{BufMut, BytesMut};
// `bytes::Buf` is not implemented for `Cow<'static, str>`, implementing it by ourselves.
// `bytes::Buf` is not implemented for `Cow<'static, str>`, implementing it by
// ourselves.
#[inline]
#[allow(clippy::ptr_arg)] // Totally okay to accept the reference to Cow here
pub fn put_cow(buf: &mut BytesMut, value: &Cow<'static, str>) {