1
0
mirror of https://github.com/actix/actix-extras.git synced 2025-08-31 11:26:59 +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,41 +1,42 @@
use std::convert::From;
use std::error::Error;
use std::fmt;
use std::str;
use std::{convert::From, error::Error, fmt, str};
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).
/// Should not be used directly unless you are implementing your own
/// [authentication scheme](super::Scheme).
#[derive(Debug)]
pub enum ParseError {
/// Header value is malformed
/// Header value is malformed.
Invalid,
/// Authentication scheme is missing
/// Authentication scheme is missing.
MissingScheme,
/// Required authentication field is missing
/// Required authentication field is missing.
MissingField(&'static str),
/// Unable to convert header into the str
/// Unable to convert header into the str.
ToStrError(header::ToStrError),
/// Malformed base64 string
/// Malformed base64 string.
Base64DecodeError(base64::DecodeError),
/// Malformed UTF-8 string
/// Malformed UTF-8 string.
Utf8Error(str::Utf8Error),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let display = match self {
ParseError::Invalid => "Invalid header value".to_string(),
ParseError::MissingScheme => "Missing authorization scheme".to_string(),
ParseError::MissingField(_) => "Missing header field".to_string(),
ParseError::ToStrError(e) => e.to_string(),
ParseError::Base64DecodeError(e) => e.to_string(),
ParseError::Utf8Error(e) => e.to_string(),
};
f.write_str(&display)
match self {
ParseError::Invalid => f.write_str("Invalid header value"),
ParseError::MissingScheme => f.write_str("Missing authorization scheme"),
ParseError::MissingField(field) => write!(f, "Missing header field ({})", field),
ParseError::ToStrError(err) => fmt::Display::fmt(err, f),
ParseError::Base64DecodeError(err) => fmt::Display::fmt(err, f),
ParseError::Utf8Error(err) => fmt::Display::fmt(err, f),
}
}
}
@@ -45,25 +46,27 @@ impl Error for ParseError {
ParseError::Invalid => None,
ParseError::MissingScheme => None,
ParseError::MissingField(_) => None,
ParseError::ToStrError(e) => Some(e),
ParseError::Base64DecodeError(e) => Some(e),
ParseError::Utf8Error(e) => Some(e),
ParseError::ToStrError(err) => Some(err),
ParseError::Base64DecodeError(err) => Some(err),
ParseError::Utf8Error(err) => Some(err),
}
}
}
impl From<header::ToStrError> for ParseError {
fn from(e: header::ToStrError) -> Self {
ParseError::ToStrError(e)
fn from(err: header::ToStrError) -> Self {
ParseError::ToStrError(err)
}
}
impl From<base64::DecodeError> for ParseError {
fn from(e: base64::DecodeError) -> Self {
ParseError::Base64DecodeError(e)
fn from(err: base64::DecodeError) -> Self {
ParseError::Base64DecodeError(err)
}
}
impl From<str::Utf8Error> for ParseError {
fn from(e: str::Utf8Error) -> Self {
ParseError::Utf8Error(e)
fn from(err: str::Utf8Error) -> Self {
ParseError::Utf8Error(err)
}
}

View File

@@ -1,27 +1,25 @@
use std::fmt;
use actix_web::error::ParseError;
use actix_web::http::header::{Header, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION};
use actix_web::HttpMessage;
use actix_web::{
error::ParseError,
http::header::{Header, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION},
HttpMessage,
};
use crate::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.
/// The "Authorization" header field allows a user agent to authenticate itself with an origin
/// serverusually, 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
/// `Authorization` is generic over an [authentication scheme](Scheme).
///
/// # Examples
/// ```
/// # use actix_web::http::header::Header;
/// # use actix_web::{HttpRequest, Result};
/// # use actix_web::{HttpRequest, Result, http::header::Header};
/// # use actix_web_httpauth::headers::authorization::{Authorization, Basic};
/// fn handler(req: HttpRequest) -> Result<String> {
/// let auth = Authorization::<Basic>::parse(&req)?;
@@ -29,49 +27,40 @@ use crate::headers::authorization::scheme::Scheme;
/// Ok(format!("Hello, {}!", auth.as_ref().user_id()))
/// }
/// ```
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Authorization<S: Scheme>(S);
impl<S> Authorization<S>
where
S: Scheme,
{
/// Consumes `Authorization` header and returns inner [`Scheme`]
/// implementation.
///
/// [`Scheme`]: ./trait.Scheme.html
impl<S: Scheme> Authorization<S> {
/// Consumes `Authorization` header and returns inner [`Scheme`] implementation.
pub fn into_scheme(self) -> S {
self.0
}
}
impl<S> From<S> for Authorization<S>
where
S: Scheme,
{
impl<S: Scheme> From<S> for Authorization<S> {
fn from(scheme: S) -> Authorization<S> {
Authorization(scheme)
}
}
impl<S> AsRef<S> for Authorization<S>
where
S: Scheme,
{
impl<S: Scheme> AsRef<S> for Authorization<S> {
fn as_ref(&self) -> &S {
&self.0
}
}
impl<S> AsMut<S> for Authorization<S>
where
S: Scheme,
{
impl<S: Scheme> AsMut<S> for Authorization<S> {
fn as_mut(&mut self) -> &mut S {
&mut self.0
}
}
impl<S: Scheme> fmt::Display for Authorization<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl<S: Scheme> Header for Authorization<S> {
#[inline]
fn name() -> HeaderName {
@@ -79,7 +68,7 @@ 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(Self::name()).ok_or(ParseError::Header)?;
let scheme = S::parse(header).map_err(|_| ParseError::Header)?;
Ok(Authorization(scheme))
@@ -93,9 +82,3 @@ impl<S: Scheme> TryIntoHeaderValue for Authorization<S> {
self.0.try_into_value()
}
}
impl<S: Scheme> fmt::Display for Authorization<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}

View File

@@ -1,4 +1,4 @@
//! `Authorization` header and various auth schemes
//! `Authorization` header and various auth schemes.
mod errors;
mod header;
@@ -6,6 +6,4 @@ mod scheme;
pub use self::errors::ParseError;
pub use self::header::Authorization;
pub use self::scheme::basic::Basic;
pub use self::scheme::bearer::Bearer;
pub use self::scheme::Scheme;
pub use self::scheme::{basic::Basic, bearer::Bearer, Scheme};

View File

@@ -8,7 +8,7 @@ use actix_web::{
use crate::headers::authorization::{errors::ParseError, Scheme};
/// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617)
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Basic {
user_id: Cow<'static, str>,
password: Option<Cow<'static, str>>,
@@ -18,8 +18,7 @@ impl Basic {
/// Creates `Basic` credentials with provided `user_id` and optional
/// `password`.
///
/// ## Example
///
/// # Examples
/// ```
/// # use actix_web_httpauth::headers::authorization::Basic;
/// let credentials = Basic::new("Alladin", Some("open sesame"));
@@ -36,13 +35,13 @@ impl Basic {
}
/// Returns client's user-ID.
pub fn user_id(&self) -> &Cow<'static, str> {
&self.user_id
pub fn user_id(&self) -> &str {
self.user_id.as_ref()
}
/// Returns client's password if provided.
pub fn password(&self) -> Option<&Cow<'static, str>> {
self.password.as_ref()
pub fn password(&self) -> Option<&str> {
self.password.as_deref()
}
}
@@ -66,6 +65,7 @@ impl Scheme for Basic {
.next()
.ok_or(ParseError::MissingField("user_id"))
.map(|user_id| user_id.to_string().into())?;
let password = credentials
.next()
.ok_or(ParseError::MissingField("password"))

View File

@@ -1,16 +1,17 @@
use std::borrow::Cow;
use std::fmt;
use std::{borrow::Cow, fmt};
use actix_web::http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue};
use actix_web::web::{BufMut, BytesMut};
use actix_web::{
http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue},
web::{BufMut, BytesMut},
};
use crate::headers::authorization::errors::ParseError;
use crate::headers::authorization::scheme::Scheme;
use crate::headers::authorization::{errors::ParseError, scheme::Scheme};
/// Credentials for `Bearer` authentication scheme, defined in [RFC6750](https://tools.ietf.org/html/rfc6750)
/// Credentials for `Bearer` authentication scheme, defined in [RFC 6750].
///
/// Should be used in combination with
/// [`Authorization`](./struct.Authorization.html) header.
/// Should be used in combination with [`Authorization`](super::Authorization) header.
///
/// [RFC 6750]: https://tools.ietf.org/html/rfc6750
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Bearer {
token: Cow<'static, str>,
@@ -19,8 +20,7 @@ pub struct Bearer {
impl Bearer {
/// Creates new `Bearer` credentials with the token provided.
///
/// ## Example
///
/// # Example
/// ```
/// # use actix_web_httpauth::headers::authorization::Bearer;
/// let credentials = Bearer::new("mF_9.B5f-4.1JqM");
@@ -35,8 +35,8 @@ impl Bearer {
}
/// Gets reference to the credentials token.
pub fn token(&self) -> &Cow<'static, str> {
&self.token
pub fn token(&self) -> &str {
self.token.as_ref()
}
}
@@ -48,8 +48,9 @@ impl Scheme for Bearer {
}
let mut parts = header.to_str()?.splitn(2, ' ');
match parts.next() {
Some(scheme) if scheme == "Bearer" => (),
Some(scheme) if scheme == "Bearer" => {}
_ => return Err(ParseError::MissingScheme),
}

View File

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

View File

@@ -1,4 +1,4 @@
//! Typed HTTP headers
//! Typed HTTP headers.
pub mod authorization;
pub mod www_authenticate;

View File

@@ -1,12 +1,11 @@
//! Challenge for the "Basic" HTTP Authentication Scheme
//! Challenge for the "Basic" HTTP Authentication Scheme.
use std::borrow::Cow;
use std::default::Default;
use std::fmt;
use std::str;
use std::{borrow::Cow, fmt, str};
use actix_web::http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue};
use actix_web::web::{BufMut, Bytes, BytesMut};
use actix_web::{
http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue},
web::{BufMut, Bytes, BytesMut},
};
use super::Challenge;
use crate::utils;
@@ -14,8 +13,7 @@ use crate::utils;
/// Challenge for [`WWW-Authenticate`] header with HTTP Basic auth scheme,
/// described in [RFC 7617](https://tools.ietf.org/html/rfc7617)
///
/// ## Example
///
/// # Examples
/// ```
/// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
/// use actix_web_httpauth::headers::www_authenticate::basic::Basic;
@@ -40,8 +38,7 @@ pub struct Basic {
impl Basic {
/// Creates new `Basic` challenge with an empty `realm` field.
///
/// ## Example
///
/// # Examples
/// ```
/// # use actix_web_httpauth::headers::www_authenticate::basic::Basic;
/// let challenge = Basic::new();
@@ -52,7 +49,7 @@ impl Basic {
/// Creates new `Basic` challenge from the provided `realm` field value.
///
/// ## Examples
/// # Examples
///
/// ```
/// # use actix_web_httpauth::headers::www_authenticate::basic::Basic;

View File

@@ -4,15 +4,15 @@ use super::{Bearer, Error};
/// Builder for the [`Bearer`] challenge.
///
/// It is up to implementor to fill all required fields,
/// neither this `Builder` or [`Bearer`] does not provide any validation.
///
/// [`Bearer`]: struct.Bearer.html
/// It is up to implementor to fill all required fields, neither this `Builder` nor [`Bearer`]
/// provide any validation.
#[derive(Debug, Default)]
pub struct BearerBuilder(Bearer);
impl BearerBuilder {
/// Provides the `scope` attribute, as defined in [RFC6749, Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3)
/// Provides the `scope` attribute, as defined in [RFC 6749 §3.3].
///
/// [RFC 6749 §3.3]: https://tools.ietf.org/html/rfc6749#section-3.3
pub fn scope<T>(mut self, value: T) -> Self
where
T: Into<Cow<'static, str>>,
@@ -21,7 +21,9 @@ impl BearerBuilder {
self
}
/// Provides the `realm` attribute, as defined in [RFC2617](https://tools.ietf.org/html/rfc2617)
/// Provides the `realm` attribute, as defined in [RFC 2617].
///
/// [RFC 2617]: https://tools.ietf.org/html/rfc2617
pub fn realm<T>(mut self, value: T) -> Self
where
T: Into<Cow<'static, str>>,
@@ -30,13 +32,17 @@ impl BearerBuilder {
self
}
/// Provides the `error` attribute, as defined in [RFC6750, Section 3.1](https://tools.ietf.org/html/rfc6750#section-3.1)
/// Provides the `error` attribute, as defined in [RFC 6750, Section 3.1].
///
/// [RFC 6750 §3.1]: https://tools.ietf.org/html/rfc6750#section-3.1
pub fn error(mut self, value: Error) -> Self {
self.0.error = Some(value);
self
}
/// Provides the `error_description` attribute, as defined in [RFC6750, Section 3](https://tools.ietf.org/html/rfc6750#section-3)
/// Provides the `error_description` attribute, as defined in [RFC 6750, Section 3].
///
/// [RFC 6750 §3]: https://tools.ietf.org/html/rfc6750#section-3
pub fn error_description<T>(mut self, value: T) -> Self
where
T: Into<Cow<'static, str>>,
@@ -45,9 +51,11 @@ impl BearerBuilder {
self
}
/// Provides the `error_uri` attribute, as defined in [RFC6750, Section 3](https://tools.ietf.org/html/rfc6750#section-3)
/// Provides the `error_uri` attribute, as defined in [RFC 6750 §3].
///
/// It is up to implementor to provide properly-formed absolute URI.
///
/// [RFC 6750 §3](https://tools.ietf.org/html/rfc6750#section-3)
pub fn error_uri<T>(mut self, value: T) -> Self
where
T: Into<Cow<'static, str>>,

View File

@@ -1,19 +1,17 @@
use std::borrow::Cow;
use std::fmt;
use std::str;
use std::{borrow::Cow, fmt, str};
use actix_web::http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue};
use actix_web::web::{BufMut, Bytes, BytesMut};
use actix_web::{
http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue},
web::{BufMut, Bytes, BytesMut},
};
use super::super::Challenge;
use super::{BearerBuilder, Error};
use crate::utils;
/// Challenge for [`WWW-Authenticate`] header with HTTP Bearer auth scheme,
/// described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3)
///
/// ## Example
/// Challenge for [`WWW-Authenticate`] header with HTTP Bearer auth scheme, described in [RFC 6750].
///
/// # Examples
/// ```
/// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
/// use actix_web_httpauth::headers::www_authenticate::bearer::{
@@ -36,8 +34,9 @@ use crate::utils;
/// }
/// ```
///
/// [`WWW-Authenticate`]: ../struct.WwwAuthenticate.html
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
/// [`WWW-Authenticate`]: crate::headers::www_authenticate::WwwAuthenticate
/// [RFC 6750]: https://tools.ietf.org/html/rfc6750#section-3
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Bearer {
pub(crate) scope: Option<Cow<'static, str>>,
pub(crate) realm: Option<Cow<'static, str>>,
@@ -49,8 +48,7 @@ pub struct Bearer {
impl Bearer {
/// Creates the builder for `Bearer` challenge.
///
/// ## Example
///
/// # Examples
/// ```
/// # use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer};
/// let challenge = Bearer::build()
@@ -71,10 +69,12 @@ impl Challenge for Bearer {
.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)
+ self.scope.as_ref().map_or(0, |scope| scope.len() + 9)
+ desc_uri_required;
let mut buffer = BytesMut::with_capacity(capacity);
buffer.put(&b"Bearer"[..]);
@@ -94,9 +94,11 @@ impl Challenge for Bearer {
let error_repr = error.as_str();
let remaining = buffer.remaining_mut();
let required = desc_uri_required + error_repr.len() + 9; // 9 is for `" error=\"\""`
if remaining < required {
buffer.reserve(required);
}
buffer.put(&b" error=\""[..]);
utils::put_quoted(&mut buffer, error_repr);
buffer.put_u8(b'"')
@@ -121,6 +123,7 @@ impl Challenge for Bearer {
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

View File

@@ -2,21 +2,20 @@ use std::fmt;
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)]
/// Bearer authorization error types, described in [RFC 6750].
///
/// [RFC 6750]: https://tools.ietf.org/html/rfc6750#section-3.1
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
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

@@ -1,4 +1,4 @@
//! Challenge for the "Bearer" HTTP Authentication Scheme
//! Challenge for the "Bearer" HTTP Authentication Scheme.
mod builder;
mod challenge;
@@ -9,4 +9,19 @@ pub use self::challenge::Bearer;
pub use self::errors::Error;
#[cfg(test)]
mod tests;
mod tests {
use super::*;
#[test]
fn to_bytes() {
let b = Bearer::build()
.error(Error::InvalidToken)
.error_description("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found")
.finish();
assert_eq!(
"Bearer error=\"invalid_token\" error_description=\"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found\"",
format!("{}", b)
);
}
}

View File

@@ -1,14 +0,0 @@
use super::*;
#[test]
fn to_bytes() {
let b = Bearer::build()
.error(Error::InvalidToken)
.error_description("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found")
.finish();
assert_eq!(
"Bearer error=\"invalid_token\" error_description=\"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found\"",
format!("{}", b)
);
}

View File

@@ -6,15 +6,17 @@ use actix_web::{
use super::Challenge;
/// `WWW-Authenticate` header, described in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.1)
/// `WWW-Authenticate` header, described in [RFC 7235].
///
/// 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.
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
/// This header is generic over the [`Challenge`] trait, see [`Basic`](super::basic::Basic) and
/// [`Bearer`](super::bearer::Bearer) challenges for details.
///
/// [RFC 7235]: https://tools.ietf.org/html/rfc7235#section-4.1
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WwwAuthenticate<C: Challenge>(pub C);
impl<C: Challenge> Header for WwwAuthenticate<C> {
#[inline]
fn name() -> HeaderName {
WWW_AUTHENTICATE
}

View File

@@ -1,9 +1,7 @@
//! `WWW-Authenticate` header and various auth challenges
//! `WWW-Authenticate` header and various auth challenges.
mod challenge;
mod header;
pub use self::challenge::basic;
pub use self::challenge::bearer;
pub use self::challenge::Challenge;
pub use self::challenge::{basic, bearer, Challenge};
pub use self::header::WwwAuthenticate;