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:
@ -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)
|
||||
}),
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user