mirror of
https://github.com/actix/actix-extras.git
synced 2025-08-31 11:26:59 +02:00
move files into module
This commit is contained in:
71
actix-web-httpauth/src/headers/authorization/errors.rs
Normal file
71
actix-web-httpauth/src/headers/authorization/errors.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::convert::From;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::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).
|
||||
#[derive(Debug)]
|
||||
pub enum ParseError {
|
||||
/// Header value is malformed
|
||||
Invalid,
|
||||
/// Authentication scheme is missing
|
||||
MissingScheme,
|
||||
/// Required authentication field is missing
|
||||
MissingField(&'static str),
|
||||
/// Unable to convert header into the str
|
||||
ToStrError(header::ToStrError),
|
||||
/// Malformed base64 string
|
||||
Base64DecodeError(base64::DecodeError),
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
ParseError::Invalid => None,
|
||||
ParseError::MissingScheme => None,
|
||||
ParseError::MissingField(_) => None,
|
||||
ParseError::ToStrError(e) => Some(e),
|
||||
ParseError::Base64DecodeError(e) => Some(e),
|
||||
ParseError::Utf8Error(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<header::ToStrError> for ParseError {
|
||||
fn from(e: header::ToStrError) -> Self {
|
||||
ParseError::ToStrError(e)
|
||||
}
|
||||
}
|
||||
impl From<base64::DecodeError> for ParseError {
|
||||
fn from(e: base64::DecodeError) -> Self {
|
||||
ParseError::Base64DecodeError(e)
|
||||
}
|
||||
}
|
||||
impl From<str::Utf8Error> for ParseError {
|
||||
fn from(e: str::Utf8Error) -> Self {
|
||||
ParseError::Utf8Error(e)
|
||||
}
|
||||
}
|
104
actix-web-httpauth/src/headers/authorization/header.rs
Normal file
104
actix-web-httpauth/src/headers/authorization/header.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use std::fmt;
|
||||
|
||||
use actix_web::error::ParseError;
|
||||
use actix_web::http::header::{
|
||||
Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION,
|
||||
};
|
||||
use actix_web::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.
|
||||
///
|
||||
/// `Authorization` header is generic over [authentication
|
||||
/// scheme](./trait.Scheme.html).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web::http::header::Header;
|
||||
/// # use actix_web::{HttpRequest, Result};
|
||||
/// # use actix_web_httpauth::headers::authorization::{Authorization, Basic};
|
||||
/// fn handler(req: HttpRequest) -> Result<String> {
|
||||
/// let auth = Authorization::<Basic>::parse(&req)?;
|
||||
///
|
||||
/// Ok(format!("Hello, {}!", auth.as_ref().user_id()))
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
|
||||
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
|
||||
pub fn into_scheme(self) -> S {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<S> for Authorization<S>
|
||||
where
|
||||
S: Scheme,
|
||||
{
|
||||
fn from(scheme: S) -> Authorization<S> {
|
||||
Authorization(scheme)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsRef<S> for Authorization<S>
|
||||
where
|
||||
S: Scheme,
|
||||
{
|
||||
fn as_ref(&self) -> &S {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsMut<S> for Authorization<S>
|
||||
where
|
||||
S: Scheme,
|
||||
{
|
||||
fn as_mut(&mut self) -> &mut S {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Scheme> Header for Authorization<S> {
|
||||
#[inline]
|
||||
fn name() -> HeaderName {
|
||||
AUTHORIZATION
|
||||
}
|
||||
|
||||
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError> {
|
||||
let header =
|
||||
msg.headers().get(AUTHORIZATION).ok_or(ParseError::Header)?;
|
||||
let scheme = S::parse(header).map_err(|_| ParseError::Header)?;
|
||||
|
||||
Ok(Authorization(scheme))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Scheme> IntoHeaderValue for Authorization<S> {
|
||||
type Error = <S as IntoHeaderValue>::Error;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
|
||||
self.0.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Scheme> fmt::Display for Authorization<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
11
actix-web-httpauth/src/headers/authorization/mod.rs
Normal file
11
actix-web-httpauth/src/headers/authorization/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! `Authorization` header and various auth schemes
|
||||
|
||||
mod errors;
|
||||
mod header;
|
||||
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;
|
204
actix-web-httpauth/src/headers/authorization/scheme/basic.rs
Normal file
204
actix-web-httpauth/src/headers/authorization/scheme/basic.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::str;
|
||||
|
||||
use actix_web::http::header::{
|
||||
HeaderValue, IntoHeaderValue, InvalidHeaderValue,
|
||||
};
|
||||
use base64;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::headers::authorization::errors::ParseError;
|
||||
use crate::headers::authorization::Scheme;
|
||||
|
||||
/// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617)
|
||||
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Basic {
|
||||
user_id: Cow<'static, str>,
|
||||
password: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl Basic {
|
||||
/// Creates `Basic` credentials with provided `user_id` and optional
|
||||
/// `password`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web_httpauth::headers::authorization::Basic;
|
||||
/// let credentials = Basic::new("Alladin", Some("open sesame"));
|
||||
/// ```
|
||||
pub fn new<U, P>(user_id: U, password: Option<P>) -> Basic
|
||||
where
|
||||
U: Into<Cow<'static, str>>,
|
||||
P: Into<Cow<'static, str>>,
|
||||
{
|
||||
Basic {
|
||||
user_id: user_id.into(),
|
||||
password: password.map(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns client's user-ID.
|
||||
pub fn user_id(&self) -> &Cow<'static, str> {
|
||||
&self.user_id
|
||||
}
|
||||
|
||||
/// Returns client's password if provided.
|
||||
pub fn password(&self) -> Option<&Cow<'static, str>> {
|
||||
self.password.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Scheme for Basic {
|
||||
fn parse(header: &HeaderValue) -> Result<Self, ParseError> {
|
||||
// "Basic *" length
|
||||
if header.len() < 7 {
|
||||
return Err(ParseError::Invalid);
|
||||
}
|
||||
|
||||
let mut parts = header.to_str()?.splitn(2, ' ');
|
||||
match parts.next() {
|
||||
Some(scheme) if scheme == "Basic" => (),
|
||||
_ => return Err(ParseError::MissingScheme),
|
||||
}
|
||||
|
||||
let decoded = base64::decode(parts.next().ok_or(ParseError::Invalid)?)?;
|
||||
let mut credentials = str::from_utf8(&decoded)?.splitn(2, ':');
|
||||
|
||||
let user_id = credentials
|
||||
.next()
|
||||
.ok_or(ParseError::MissingField("user_id"))
|
||||
.map(|user_id| user_id.to_string().into())?;
|
||||
let password = credentials
|
||||
.next()
|
||||
.ok_or(ParseError::MissingField("password"))
|
||||
.map(|password| {
|
||||
if password.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password.to_string().into())
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Basic {
|
||||
user_id,
|
||||
password,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Basic {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_fmt(format_args!("Basic {}:******", self.user_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Basic {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_fmt(format_args!("Basic {}:******", self.user_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Basic {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
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()),
|
||||
);
|
||||
|
||||
credentials.extend_from_slice(self.user_id.as_bytes());
|
||||
credentials.put_u8(b':');
|
||||
if let Some(ref password) = self.password {
|
||||
credentials.extend_from_slice(password.as_bytes());
|
||||
}
|
||||
|
||||
// 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(&b"Basic "[..]);
|
||||
value.put(&encoded.as_bytes()[..]);
|
||||
|
||||
HeaderValue::from_maybe_shared(value.freeze())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Basic, Scheme};
|
||||
use actix_web::http::header::{HeaderValue, IntoHeaderValue};
|
||||
|
||||
#[test]
|
||||
fn test_parse_header() {
|
||||
let value =
|
||||
HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
|
||||
let scheme = Basic::parse(&value);
|
||||
|
||||
assert!(scheme.is_ok());
|
||||
let scheme = scheme.unwrap();
|
||||
assert_eq!(scheme.user_id, "Aladdin");
|
||||
assert_eq!(scheme.password, Some("open sesame".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_password() {
|
||||
let value = HeaderValue::from_static("Basic QWxhZGRpbjo=");
|
||||
let scheme = Basic::parse(&value);
|
||||
|
||||
assert!(scheme.is_ok());
|
||||
let scheme = scheme.unwrap();
|
||||
assert_eq!(scheme.user_id, "Aladdin");
|
||||
assert_eq!(scheme.password, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_header() {
|
||||
let value = HeaderValue::from_static("");
|
||||
let scheme = Basic::parse(&value);
|
||||
|
||||
assert!(scheme.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrong_scheme() {
|
||||
let value = HeaderValue::from_static("THOUSHALLNOTPASS please?");
|
||||
let scheme = Basic::parse(&value);
|
||||
|
||||
assert!(scheme.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_credentials() {
|
||||
let value = HeaderValue::from_static("Basic ");
|
||||
let scheme = Basic::parse(&value);
|
||||
|
||||
assert!(scheme.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_credentials_colon() {
|
||||
let value = HeaderValue::from_static("Basic QWxsYWRpbg==");
|
||||
let scheme = Basic::parse(&value);
|
||||
|
||||
assert!(scheme.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_header_value() {
|
||||
let basic = Basic {
|
||||
user_id: "Aladdin".into(),
|
||||
password: Some("open sesame".into()),
|
||||
};
|
||||
|
||||
let result = basic.try_into();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
|
||||
);
|
||||
}
|
||||
}
|
140
actix-web-httpauth/src/headers/authorization/scheme/bearer.rs
Normal file
140
actix-web-httpauth/src/headers/authorization/scheme/bearer.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
use actix_web::http::header::{
|
||||
HeaderValue, IntoHeaderValue, InvalidHeaderValue,
|
||||
};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::headers::authorization::errors::ParseError;
|
||||
use crate::headers::authorization::scheme::Scheme;
|
||||
|
||||
/// 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.
|
||||
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Bearer {
|
||||
token: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl Bearer {
|
||||
/// Creates new `Bearer` credentials with the token provided.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web_httpauth::headers::authorization::Bearer;
|
||||
/// let credentials = Bearer::new("mF_9.B5f-4.1JqM");
|
||||
/// ```
|
||||
pub fn new<T>(token: T) -> Bearer
|
||||
where
|
||||
T: Into<Cow<'static, str>>,
|
||||
{
|
||||
Bearer {
|
||||
token: token.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets reference to the credentials token.
|
||||
pub fn token(&self) -> &Cow<'static, str> {
|
||||
&self.token
|
||||
}
|
||||
}
|
||||
|
||||
impl Scheme for Bearer {
|
||||
fn parse(header: &HeaderValue) -> Result<Self, ParseError> {
|
||||
// "Bearer *" length
|
||||
if header.len() < 8 {
|
||||
return Err(ParseError::Invalid);
|
||||
}
|
||||
|
||||
let mut parts = header.to_str()?.splitn(2, ' ');
|
||||
match parts.next() {
|
||||
Some(scheme) if scheme == "Bearer" => (),
|
||||
_ => return Err(ParseError::MissingScheme),
|
||||
}
|
||||
|
||||
let token = parts.next().ok_or(ParseError::Invalid)?;
|
||||
|
||||
Ok(Bearer {
|
||||
token: token.to_string().into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Bearer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_fmt(format_args!("Bearer ******"))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Bearer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_fmt(format_args!("Bearer {}", self.token))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Bearer {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
|
||||
let mut buffer = BytesMut::with_capacity(7 + self.token.len());
|
||||
buffer.put(&b"Bearer "[..]);
|
||||
buffer.extend_from_slice(self.token.as_bytes());
|
||||
|
||||
HeaderValue::from_maybe_shared(buffer.freeze())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Bearer, Scheme};
|
||||
use actix_web::http::header::{HeaderValue, IntoHeaderValue};
|
||||
|
||||
#[test]
|
||||
fn test_parse_header() {
|
||||
let value = HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM");
|
||||
let scheme = Bearer::parse(&value);
|
||||
|
||||
assert!(scheme.is_ok());
|
||||
let scheme = scheme.unwrap();
|
||||
assert_eq!(scheme.token, "mF_9.B5f-4.1JqM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_header() {
|
||||
let value = HeaderValue::from_static("");
|
||||
let scheme = Bearer::parse(&value);
|
||||
|
||||
assert!(scheme.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrong_scheme() {
|
||||
let value = HeaderValue::from_static("OAuthToken foo");
|
||||
let scheme = Bearer::parse(&value);
|
||||
|
||||
assert!(scheme.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_token() {
|
||||
let value = HeaderValue::from_static("Bearer ");
|
||||
let scheme = Bearer::parse(&value);
|
||||
|
||||
assert!(scheme.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_header_value() {
|
||||
let bearer = Bearer::new("mF_9.B5f-4.1JqM");
|
||||
|
||||
let result = bearer.try_into();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM")
|
||||
);
|
||||
}
|
||||
}
|
17
actix-web-httpauth/src/headers/authorization/scheme/mod.rs
Normal file
17
actix-web-httpauth/src/headers/authorization/scheme/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use actix_web::http::header::{HeaderValue, IntoHeaderValue};
|
||||
|
||||
pub mod basic;
|
||||
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
|
||||
{
|
||||
/// Try to parse the authentication scheme from the `Authorization` header.
|
||||
fn parse(header: &HeaderValue) -> Result<Self, ParseError>;
|
||||
}
|
4
actix-web-httpauth/src/headers/mod.rs
Normal file
4
actix-web-httpauth/src/headers/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
//! Typed HTTP headers
|
||||
|
||||
pub mod authorization;
|
||||
pub mod www_authenticate;
|
@@ -0,0 +1,144 @@
|
||||
//! Challenge for the "Basic" HTTP Authentication Scheme
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::default::Default;
|
||||
use std::fmt;
|
||||
use std::str;
|
||||
|
||||
use actix_web::http::header::{
|
||||
HeaderValue, IntoHeaderValue, InvalidHeaderValue,
|
||||
};
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
|
||||
use super::Challenge;
|
||||
use crate::utils;
|
||||
|
||||
/// Challenge for [`WWW-Authenticate`] header with HTTP Basic auth scheme,
|
||||
/// described in [RFC 7617](https://tools.ietf.org/html/rfc7617)
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
|
||||
/// use actix_web_httpauth::headers::www_authenticate::basic::Basic;
|
||||
/// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate;
|
||||
///
|
||||
/// fn index(_req: HttpRequest) -> HttpResponse {
|
||||
/// let challenge = Basic::with_realm("Restricted area");
|
||||
///
|
||||
/// HttpResponse::Unauthorized()
|
||||
/// .set(WwwAuthenticate(challenge))
|
||||
/// .finish()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`WWW-Authenticate`]: ../struct.WwwAuthenticate.html
|
||||
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
|
||||
pub struct Basic {
|
||||
// "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A
|
||||
pub(crate) realm: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl Basic {
|
||||
/// Creates new `Basic` challenge with an empty `realm` field.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web_httpauth::headers::www_authenticate::basic::Basic;
|
||||
/// let challenge = Basic::new();
|
||||
/// ```
|
||||
pub fn new() -> Basic {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Creates new `Basic` challenge from the provided `realm` field value.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web_httpauth::headers::www_authenticate::basic::Basic;
|
||||
/// let challenge = Basic::with_realm("Restricted area");
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web_httpauth::headers::www_authenticate::basic::Basic;
|
||||
/// let my_realm = "Earth realm".to_string();
|
||||
/// let challenge = Basic::with_realm(my_realm);
|
||||
/// ```
|
||||
pub fn with_realm<T>(value: T) -> Basic
|
||||
where
|
||||
T: Into<Cow<'static, str>>,
|
||||
{
|
||||
Basic {
|
||||
realm: Some(value.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl Challenge for Basic {
|
||||
fn to_bytes(&self) -> Bytes {
|
||||
// 5 is for `"Basic"`, 9 is for `"realm=\"\""`
|
||||
let length = 5 + self.realm.as_ref().map_or(0, |realm| realm.len() + 9);
|
||||
let mut buffer = BytesMut::with_capacity(length);
|
||||
buffer.put(&b"Basic"[..]);
|
||||
if let Some(ref realm) = self.realm {
|
||||
buffer.put(&b" realm=\""[..]);
|
||||
utils::put_quoted(&mut buffer, realm);
|
||||
buffer.put_u8(b'"');
|
||||
}
|
||||
|
||||
buffer.freeze()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Basic {
|
||||
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
|
||||
.map_err(|_| fmt::Error)?;
|
||||
|
||||
f.write_str(repr)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Basic {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
|
||||
HeaderValue::from_maybe_shared(self.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Basic;
|
||||
use actix_web::http::header::IntoHeaderValue;
|
||||
|
||||
#[test]
|
||||
fn test_plain_into_header_value() {
|
||||
let challenge = Basic {
|
||||
realm: None,
|
||||
};
|
||||
|
||||
let value = challenge.try_into();
|
||||
assert!(value.is_ok());
|
||||
let value = value.unwrap();
|
||||
assert_eq!(value, "Basic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_realm_into_header_value() {
|
||||
let challenge = Basic {
|
||||
realm: Some("Restricted area".into()),
|
||||
};
|
||||
|
||||
let value = challenge.try_into();
|
||||
assert!(value.is_ok());
|
||||
let value = value.unwrap();
|
||||
assert_eq!(value, "Basic realm=\"Restricted area\"");
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
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
|
||||
#[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)
|
||||
pub fn scope<T>(mut self, value: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'static, str>>,
|
||||
{
|
||||
self.0.scope = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Provides the `realm` attribute, as defined in [RFC2617](https://tools.ietf.org/html/rfc2617)
|
||||
pub fn realm<T>(mut self, value: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'static, str>>,
|
||||
{
|
||||
self.0.realm = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Provides the `error` attribute, as defined in [RFC6750, Section 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)
|
||||
pub fn error_description<T>(mut self, value: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'static, str>>,
|
||||
{
|
||||
self.0.error_description = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Provides the `error_uri` attribute, as defined in [RFC6750, Section 3](https://tools.ietf.org/html/rfc6750#section-3)
|
||||
///
|
||||
/// It is up to implementor to provide properly-formed absolute URI.
|
||||
pub fn error_uri<T>(mut self, value: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'static, str>>,
|
||||
{
|
||||
self.0.error_uri = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Consumes the builder and returns built `Bearer` instance.
|
||||
pub fn finish(self) -> Bearer {
|
||||
self.0
|
||||
}
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::str;
|
||||
|
||||
use actix_web::http::header::{
|
||||
HeaderValue, IntoHeaderValue, InvalidHeaderValue,
|
||||
};
|
||||
use bytes::{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
|
||||
///
|
||||
/// ```
|
||||
/// # 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::WwwAuthenticate;
|
||||
///
|
||||
/// fn index(_req: HttpRequest) -> HttpResponse {
|
||||
/// let challenge = Bearer::build()
|
||||
/// .realm("example")
|
||||
/// .scope("openid profile email")
|
||||
/// .error(Error::InvalidToken)
|
||||
/// .error_description("The access token expired")
|
||||
/// .error_uri("http://example.org")
|
||||
/// .finish();
|
||||
///
|
||||
/// HttpResponse::Unauthorized()
|
||||
/// .set(WwwAuthenticate(challenge))
|
||||
/// .finish()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`WWW-Authenticate`]: ../struct.WwwAuthenticate.html
|
||||
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
|
||||
pub struct Bearer {
|
||||
pub(crate) scope: Option<Cow<'static, str>>,
|
||||
pub(crate) realm: Option<Cow<'static, str>>,
|
||||
pub(crate) error: Option<Error>,
|
||||
pub(crate) error_description: Option<Cow<'static, str>>,
|
||||
pub(crate) error_uri: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl Bearer {
|
||||
/// Creates the builder for `Bearer` challenge.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer};
|
||||
/// let challenge = Bearer::build()
|
||||
/// .realm("Restricted area")
|
||||
/// .scope("openid profile email")
|
||||
/// .finish();
|
||||
/// ```
|
||||
pub fn build() -> BearerBuilder {
|
||||
BearerBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[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)
|
||||
+ 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"[..]);
|
||||
|
||||
if let Some(ref realm) = self.realm {
|
||||
buffer.put(&b" realm=\""[..]);
|
||||
utils::put_quoted(&mut buffer, realm);
|
||||
buffer.put_u8(b'"');
|
||||
}
|
||||
|
||||
if let Some(ref scope) = self.scope {
|
||||
buffer.put(&b" scope=\""[..]);
|
||||
utils::put_quoted(&mut buffer, scope);
|
||||
buffer.put_u8(b'"');
|
||||
}
|
||||
|
||||
if let Some(ref error) = self.error {
|
||||
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'"')
|
||||
}
|
||||
|
||||
if let Some(ref error_description) = self.error_description {
|
||||
buffer.put(&b" error_description=\""[..]);
|
||||
utils::put_quoted(&mut buffer, error_description);
|
||||
buffer.put_u8(b'"');
|
||||
}
|
||||
|
||||
if let Some(ref error_uri) = self.error_uri {
|
||||
buffer.put(&b" error_uri=\""[..]);
|
||||
utils::put_quoted(&mut buffer, error_uri);
|
||||
buffer.put_u8(b'"');
|
||||
}
|
||||
|
||||
buffer.freeze()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.map_err(|_| fmt::Error)?;
|
||||
|
||||
f.write_str(repr)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Bearer {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
|
||||
HeaderValue::from_maybe_shared(self.to_bytes())
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
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)]
|
||||
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.
|
||||
InvalidRequest,
|
||||
|
||||
/// 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.
|
||||
InsufficientScope,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Returns [HTTP status code] suitable for current error type.
|
||||
///
|
||||
/// [HTTP status code]: `actix_web::http::StatusCode`
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Error::InvalidRequest => StatusCode::BAD_REQUEST,
|
||||
Error::InvalidToken => StatusCode::UNAUTHORIZED,
|
||||
Error::InsufficientScope => StatusCode::FORBIDDEN,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Error::InvalidRequest => "invalid_request",
|
||||
Error::InvalidToken => "invalid_token",
|
||||
Error::InsufficientScope => "insufficient_scope",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
//! Challenge for the "Bearer" HTTP Authentication Scheme
|
||||
|
||||
mod builder;
|
||||
mod challenge;
|
||||
mod errors;
|
||||
|
||||
pub use self::builder::BearerBuilder;
|
||||
pub use self::challenge::Bearer;
|
||||
pub use self::errors::Error;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
@@ -0,0 +1,16 @@
|
||||
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)
|
||||
);
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use actix_web::http::header::IntoHeaderValue;
|
||||
use bytes::Bytes;
|
||||
|
||||
pub mod basic;
|
||||
pub mod bearer;
|
||||
|
||||
/// Authentication challenge for `WWW-Authenticate` header.
|
||||
pub trait Challenge:
|
||||
IntoHeaderValue + Debug + Display + Clone + Send + Sync
|
||||
{
|
||||
/// Converts the challenge into a bytes suitable for HTTP transmission.
|
||||
fn to_bytes(&self) -> Bytes;
|
||||
}
|
33
actix-web-httpauth/src/headers/www_authenticate/header.rs
Normal file
33
actix-web-httpauth/src/headers/www_authenticate/header.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use actix_web::error::ParseError;
|
||||
use actix_web::http::header::{
|
||||
Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE,
|
||||
};
|
||||
use actix_web::HttpMessage;
|
||||
|
||||
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.
|
||||
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
|
||||
pub struct WwwAuthenticate<C: Challenge>(pub C);
|
||||
|
||||
impl<C: Challenge> Header for WwwAuthenticate<C> {
|
||||
fn name() -> HeaderName {
|
||||
WWW_AUTHENTICATE
|
||||
}
|
||||
|
||||
fn parse<T: HttpMessage>(_msg: &T) -> Result<Self, ParseError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Challenge> IntoHeaderValue for WwwAuthenticate<C> {
|
||||
type Error = <C as IntoHeaderValue>::Error;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
|
||||
self.0.try_into()
|
||||
}
|
||||
}
|
9
actix-web-httpauth/src/headers/www_authenticate/mod.rs
Normal file
9
actix-web-httpauth/src/headers/www_authenticate/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! `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::header::WwwAuthenticate;
|
Reference in New Issue
Block a user