1
0
mirror of https://github.com/actix/actix-extras.git synced 2025-06-27 02:37:42 +02:00

improve httpauth ergonomics (#264)

* improve httpauth ergonomics

* update changelog

* code and docs cleanup

* docs

* docs clean

* remove AuthExtractor trait

* update changelog
This commit is contained in:
Rob Ede
2022-07-21 03:50:22 +02:00
committed by GitHub
parent 4d2f4d58b4
commit ff06958b32
25 changed files with 296 additions and 462 deletions

View File

@ -1,32 +1,27 @@
//! Extractor for the "Basic" HTTP Authentication Scheme
//! Extractor for the "Basic" HTTP Authentication Scheme.
use std::borrow::Cow;
use actix_utils::future::{ready, Ready};
use actix_web::dev::{Payload, ServiceRequest};
use actix_web::http::header::Header;
use actix_web::{FromRequest, HttpRequest};
use actix_web::{dev::Payload, http::header::Header, FromRequest, HttpRequest};
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;
use super::{config::AuthExtractorConfig, errors::AuthenticationError};
use crate::headers::{
authorization::{Authorization, Basic},
www_authenticate::basic::Basic as Challenge,
};
/// [`BasicAuth`] extractor configuration,
/// used for [`WWW-Authenticate`] header later.
/// [`BasicAuth`] extractor configuration used for [`WWW-Authenticate`] header later.
///
/// [`BasicAuth`]: ./struct.BasicAuth.html
/// [`WWW-Authenticate`]:
/// ../../headers/www_authenticate/struct.WwwAuthenticate.html
/// [`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 [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
/// [RFC 2617 §1.2](https://tools.ietf.org/html/rfc2617#section-1.2).
pub fn realm<T>(mut self, value: T) -> Config
where
T: Into<Cow<'static, str>>,
@ -50,14 +45,10 @@ impl AuthExtractorConfig for Config {
}
}
// Needs `fn main` to display complete example.
#[allow(clippy::needless_doctest_main)]
/// Extractor for HTTP Basic auth.
///
/// # Example
///
/// # Examples
/// ```
/// use actix_web::Result;
/// use actix_web_httpauth::extractors::basic::BasicAuth;
///
/// async fn index(auth: BasicAuth) -> String {
@ -65,41 +56,36 @@ impl AuthExtractorConfig 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.
///
/// ## Example
/// 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::{BasicAuth, Config};
/// use actix_web_httpauth::extractors::basic::{self, BasicAuth};
///
/// async fn index(auth: BasicAuth) -> String {
/// format!("Hello, {}!", auth.user_id())
/// }
///
/// fn main() {
/// let app = App::new()
/// .app_data(Config::default().realm("Restricted area"))
/// .service(web::resource("/index.html").route(web::get().to(index)));
/// }
/// App::new()
/// .app_data(basic::Config::default().realm("Restricted area"))
/// .service(web::resource("/index.html").route(web::get().to(index)));
/// ```
///
/// [`Config`]: ./struct.Config.html
/// [app data]: https://docs.rs/actix-web/1.0.0-beta.5/actix_web/struct.App.html#method.data
/// [app data]: https://docs.rs/actix-web/4/actix_web/struct.App.html#method.app_data
#[derive(Debug, Clone)]
pub struct BasicAuth(Basic);
impl BasicAuth {
/// Returns client's user-ID.
pub fn user_id(&self) -> &str {
self.0.user_id().as_ref()
self.0.user_id()
}
/// Returns client's password.
pub fn password(&self) -> Option<&str> {
self.0.password().map(|s| s.as_ref())
self.0.password()
}
}
@ -111,35 +97,13 @@ impl FromRequest for BasicAuth {
ready(
Authorization::<Basic>::parse(req)
.map(|auth| BasicAuth(auth.into_scheme()))
.map_err(|_| {
// TODO: debug! the original error
.map_err(|err| {
log::debug!("`BasicAuth` extract error: {}", err);
let challenge = req
.app_data::<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 = Ready<Result<Self, Self::Error>>;
fn from_service_request(req: &ServiceRequest) -> Self::Future {
ready(
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);
.unwrap_or_default();
AuthenticationError::new(challenge)
}),

View File

@ -1,19 +1,15 @@
//! Extractor for the "Bearer" HTTP Authentication Scheme
//! Extractor for the "Bearer" HTTP Authentication Scheme.
use std::{borrow::Cow, default::Default};
use actix_utils::future::{ready, Ready};
use actix_web::{
dev::{Payload, ServiceRequest},
http::header::Header,
FromRequest, HttpRequest,
};
use actix_web::{dev::Payload, http::header::Header, FromRequest, HttpRequest};
use super::{config::AuthExtractorConfig, errors::AuthenticationError, AuthExtractor};
use super::{config::AuthExtractorConfig, errors::AuthenticationError};
pub use crate::headers::www_authenticate::bearer::Error;
use crate::headers::{authorization, www_authenticate::bearer};
/// [BearerAuth](./struct/BearerAuth.html) extractor configuration.
/// [`BearerAuth`] extractor configuration.
#[derive(Debug, Clone, Default)]
pub struct Config(bearer::Bearer);
@ -31,7 +27,7 @@ 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).
/// described in HTTP/1.1 [RFC 2617](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
@ -52,12 +48,9 @@ impl AuthExtractorConfig for Config {
}
}
// Needs `fn main` to display complete example.
#[allow(clippy::needless_doctest_main)]
/// Extractor for HTTP Bearer auth
///
/// # Example
///
/// # Examples
/// ```
/// use actix_web_httpauth::extractors::bearer::BearerAuth;
///
@ -70,25 +63,22 @@ impl AuthExtractorConfig for Config {
/// from the [app data] in order to properly form the `WWW-Authenticate`
/// response header.
///
/// ## Example
///
/// # Examples
/// ```
/// use actix_web::{web, App};
/// use actix_web_httpauth::extractors::bearer::{BearerAuth, Config};
/// use actix_web_httpauth::extractors::bearer::{self, BearerAuth};
///
/// async fn index(auth: BearerAuth) -> String {
/// format!("Hello, {}!", auth.token())
/// }
///
/// fn main() {
/// let app = App::new()
/// .app_data(
/// Config::default()
/// .realm("Restricted area")
/// .scope("email photo"),
/// )
/// .service(web::resource("/index.html").route(web::get().to(index)));
/// }
/// App::new()
/// .app_data(
/// bearer::Config::default()
/// .realm("Restricted area")
/// .scope("email photo"),
/// )
/// .service(web::resource("/index.html").route(web::get().to(index)));
/// ```
#[derive(Debug, Clone)]
pub struct BearerAuth(authorization::Bearer);
@ -120,26 +110,6 @@ impl FromRequest for BearerAuth {
}
}
impl AuthExtractor for BearerAuth {
type Future = Ready<Result<Self, Self::Error>>;
type Error = AuthenticationError<bearer::Bearer>;
fn from_service_request(req: &ServiceRequest) -> Self::Future {
ready(
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.

View File

@ -1,10 +1,8 @@
use super::AuthenticationError;
use crate::headers::www_authenticate::Challenge;
/// Trait implemented for types that provides configuration
/// for the authentication [extractors].
///
/// [extractors]: ./trait.AuthExtractor.html
/// Trait implemented for types that provides configuration for the authentication
/// [extractors](super::AuthExtractor).
pub trait AuthExtractorConfig {
/// Associated challenge type.
type Inner: Challenge;

View File

@ -1,16 +1,13 @@
use std::error::Error;
use std::fmt;
use std::{error::Error, fmt};
use actix_web::http::StatusCode;
use actix_web::{HttpResponse, ResponseError};
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use crate::headers::www_authenticate::Challenge;
use crate::headers::www_authenticate::WwwAuthenticate;
use crate::headers::www_authenticate::{Challenge, WwwAuthenticate};
/// Authentication error returned by authentication extractors.
///
/// Different extractors may extend `AuthenticationError` implementation
/// in order to provide access to inner challenge fields.
/// Different extractors may extend `AuthenticationError` implementation in order to provide access
/// inner challenge fields.
#[derive(Debug)]
pub struct AuthenticationError<C: Challenge> {
challenge: C,
@ -35,8 +32,8 @@ impl<C: Challenge> AuthenticationError<C> {
/// Returns mutable reference to the inner status code.
///
/// Can be used to override returned status code, but by default
/// this lib tries to stick to the RFC, so it might be unreasonable.
/// Can be used to override returned status code, but by default this lib tries to stick to the
/// RFC, so it might be unreasonable.
pub fn status_code_mut(&mut self) -> &mut StatusCode {
&mut self.status_code
}
@ -48,19 +45,18 @@ impl<C: Challenge> fmt::Display for AuthenticationError<C> {
}
}
impl<C: 'static + Challenge> Error for AuthenticationError<C> {}
impl<C: 'static + Challenge> ResponseError for AuthenticationError<C> {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code)
// TODO: Get rid of the `.clone()`
.insert_header(WwwAuthenticate(self.challenge.clone()))
.finish()
}
impl<C: Challenge + 'static> Error for AuthenticationError<C> {}
impl<C: Challenge + 'static> ResponseError for AuthenticationError<C> {
fn status_code(&self) -> StatusCode {
self.status_code
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(WwwAuthenticate(self.challenge.clone()))
.finish()
}
}
#[cfg(test)]
@ -72,12 +68,12 @@ mod tests {
#[test]
fn test_status_code_is_preserved_across_error_conversions() {
let ae: AuthenticationError<Basic> = AuthenticationError::new(Basic::default());
let ae = AuthenticationError::new(Basic::default());
let expected = ae.status_code;
// Converting the AuthenticationError into a ResponseError should preserve the status code.
let e = Error::from(ae);
let re = e.as_response_error();
assert_eq!(expected, re.status_code());
let err = Error::from(ae);
let res_err = err.as_response_error();
assert_eq!(expected, res_err.status_code());
}
}

View File

@ -1,15 +1,4 @@
//! Type-safe authentication information extractors
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use actix_web::dev::ServiceRequest;
use actix_web::Error;
use futures_core::ready;
use pin_project_lite::pin_project;
//! Type-safe authentication information extractors.
pub mod basic;
pub mod bearer;
@ -18,86 +7,3 @@ 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: Future<Output = Result<Self, Self::Error>>;
/// Parse the authentication credentials from the actix' `ServiceRequest`.
fn from_service_request(req: &ServiceRequest) -> Self::Future;
}
impl<T: AuthExtractor> AuthExtractor for Option<T> {
type Error = T::Error;
type Future = AuthExtractorOptFut<T::Future>;
fn from_service_request(req: &ServiceRequest) -> Self::Future {
let fut = T::from_service_request(req);
AuthExtractorOptFut { fut }
}
}
pin_project! {
#[doc(hidden)]
pub struct AuthExtractorOptFut<F> {
#[pin]
fut: F
}
}
impl<F, T, E> Future for AuthExtractorOptFut<F>
where
F: Future<Output = Result<T, E>>,
{
type Output = Result<Option<T>, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let res = ready!(self.project().fut.poll(cx));
Poll::Ready(Ok(res.ok()))
}
}
impl<T: AuthExtractor> AuthExtractor for Result<T, T::Error> {
type Error = T::Error;
type Future = AuthExtractorResFut<T::Future>;
fn from_service_request(req: &ServiceRequest) -> Self::Future {
AuthExtractorResFut {
fut: T::from_service_request(req),
}
}
}
pin_project! {
#[doc(hidden)]
pub struct AuthExtractorResFut<F> {
#[pin]
fut: F
}
}
impl<F, T, E> Future for AuthExtractorResFut<F>
where
F: Future<Output = Result<T, E>>,
{
type Output = Result<F::Output, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let res = ready!(self.project().fut.poll(cx));
Poll::Ready(Ok(res))
}
}