1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-23 23:51:06 +01:00

Upgrage to the actix-web 1.0 -- alpha.1 version

This commit is contained in:
svartalf 2019-05-15 17:38:55 +03:00
parent cb158fbe8d
commit 7a9b3924cb
34 changed files with 874 additions and 521 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

View File

@ -4,6 +4,3 @@ rust:
- stable - stable
- beta - beta
- nightly - nightly
matrix:
allow_failures:
- rust: nightly

View File

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## Unreleased
- Crate edition was changed to `2018`, same as `actix-web`
- Depends on `actix-web = "^1.0"` version now
- `WWWAuthenticate` header struct was renamed into `WwwAuthenticate`
- Challenges and extractor configs are now operating with `Cow<'static, str>` types instead of `String` types
## [0.2.0] - 2019-04-26 ## [0.2.0] - 2019-04-26
### Changed ### Changed
- `actix-web` dependency is used without default features now ([#6](https://github.com/svartalf/actix-web-httpauth/pull/6)) - `actix-web` dependency is used without default features now ([#6](https://github.com/svartalf/actix-web-httpauth/pull/6))

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-httpauth" name = "actix-web-httpauth"
version = "0.2.0" version = "0.3.0-alpha.1"
authors = ["svartalf <self@svartalf.info>"] authors = ["svartalf <self@svartalf.info>"]
description = "HTTP authentication schemes for actix-web" description = "HTTP authentication schemes for actix-web"
readme = "README.md" readme = "README.md"
@ -9,14 +9,19 @@ homepage = "https://github.com/svartalf/actix-web-httpauth"
repository = "https://github.com/svartalf/actix-web-httpauth.git" repository = "https://github.com/svartalf/actix-web-httpauth.git"
documentation = "https://docs.rs/actix-web-httpauth/" documentation = "https://docs.rs/actix-web-httpauth/"
categories = ["web-programming::http-server"] categories = ["web-programming::http-server"]
license = "MIT/Apache-2.0" license = "MIT OR Apache-2.0"
exclude = [".travis.yml", ".gitignore"] exclude = [".travis.yml", ".gitignore"]
edition = "2018"
[dependencies] [dependencies]
actix-web = { version = "0.7", default_features = false } actix-web = { version = "1.0.0-beta.5", default_features = false }
bytes = "0.4" bytes = "0.4"
base64 = "0.10" base64 = "0.10"
[dev-dependencies]
futures = "0.1.27"
actix-service = "0.4.0"
[features] [features]
default = [] default = []
nightly = [] nightly = []

View File

@ -1,36 +0,0 @@
extern crate actix_web;
extern crate actix_web_httpauth;
use actix_web::{server, App, Result, HttpRequest, FromRequest};
use actix_web::middleware::{Middleware, Started};
use actix_web_httpauth::extractors::basic::{BasicAuth, Config};
use actix_web_httpauth::extractors::AuthenticationError;
struct Auth;
impl<S> Middleware<S> for Auth {
fn start(&self, req: &HttpRequest<S>) -> Result<Started> {
let mut config = Config::default();
config.realm("WallyWorld");
let auth = BasicAuth::from_request(&req, &config)?;
if auth.username() == "Aladdin" && auth.password() == Some("open sesame") {
Ok(Started::Done)
} else {
Err(AuthenticationError::from(config).into())
}
}
}
fn index(_req: HttpRequest) -> String {
"Hello, authorized user!".to_string()
}
fn main() {
server::new(|| App::new()
.middleware(Auth)
.resource("/", |r| r.with(index))
)
.bind("127.0.0.1:8088").unwrap()
.run();
}

View File

@ -1,40 +0,0 @@
extern crate actix_web;
extern crate actix_web_httpauth;
use actix_web::{server, App, HttpRequest, Result, FromRequest};
use actix_web_httpauth::extractors::AuthenticationError;
use actix_web_httpauth::extractors::bearer::{BearerAuth, Config, Error};
use actix_web::middleware::{Middleware, Started};
struct Auth;
impl<S> Middleware<S> for Auth {
fn start(&self, req: &HttpRequest<S>) -> Result<Started> {
let mut config = Config::default();
config.realm("Restricted area");
config.scope("openid profile email");
let auth = BearerAuth::from_request(&req, &config)?;
if auth.token() == "mF_9.B5f-4.1JqM" {
Ok(Started::Done)
} else {
Err(AuthenticationError::from(config)
.with_error(Error::InvalidToken)
.into())
}
}
}
fn index(_req: HttpRequest) -> String {
"Hello, authorized user!".to_string()
}
fn main() {
server::new(|| App::new()
.middleware(Auth)
.resource("/", |r| r.with(index))
)
.bind("127.0.0.1:8088").unwrap()
.run();
}

View File

@ -1,25 +0,0 @@
extern crate actix_web;
extern crate actix_web_httpauth;
use actix_web::{server, App, HttpRequest, HttpResponse};
use actix_web::http::StatusCode;
use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate};
use actix_web_httpauth::headers::www_authenticate::basic::Basic;
fn index(req: HttpRequest) -> HttpResponse {
let challenge = Basic {
realm: Some("Restricted area".to_string()),
};
req.build_response(StatusCode::UNAUTHORIZED)
.set(WWWAuthenticate(challenge))
.finish()
}
fn main() {
server::new(|| App::new()
.resource("/", |r| r.with(index)))
.bind("127.0.0.1:8088").unwrap()
.run();
}

View File

@ -1,29 +0,0 @@
extern crate actix_web;
extern crate actix_web_httpauth;
use actix_web::{server, App, HttpRequest, HttpResponse};
use actix_web::http::StatusCode;
use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate};
use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer, Error};
fn index(req: HttpRequest) -> HttpResponse {
let challenge = Bearer {
realm: Some("example".to_string()),
scope: Some("openid profile email".to_string()),
error: Some(Error::InvalidToken),
error_description: Some("The access token expired".to_string()),
error_uri: Some("http://example.org".to_string()),
};
req.build_response(StatusCode::UNAUTHORIZED)
.set(WWWAuthenticate(challenge))
.finish()
}
fn main() {
server::new(|| App::new()
.resource("/", |r| r.with(index)))
.bind("127.0.0.1:8088").unwrap()
.run();
}

View File

@ -0,0 +1,89 @@
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()
}

9
rustfmt.toml Normal file
View File

@ -0,0 +1,9 @@
unstable_features = true
edition = "2018"
version = "Two"
wrap_comments = true
comment_width = 120
max_width = 120
merge_imports = false
newline_style = "Unix"
struct_lit_single_line = false

View File

@ -1,16 +1,22 @@
use std::default::Default; //! Extractor for the "Basic" HTTP Authentication Scheme
use actix_web::{HttpRequest, FromRequest}; use std::borrow::Cow;
use actix_web::dev::{Payload, ServiceRequest};
use actix_web::http::header::Header; use actix_web::http::header::Header;
use actix_web::{FromRequest, HttpRequest};
use headers::authorization::{Authorization, Basic};
use headers::www_authenticate::basic::Basic as Challenge;
use super::errors::AuthenticationError;
use super::config::ExtractorConfig; use super::config::ExtractorConfig;
use super::errors::AuthenticationError;
use crate::headers::authorization::{Authorization, Basic};
use crate::headers::www_authenticate::basic::Basic as Challenge;
/// [`BasicAuth`](./struct.BasicAuth.html) extractor configuration, /// [`BasicAuth`] extractor configuration,
/// used for `WWW-Authenticate` header later. /// used for [`WWW-Authenticate`] header later.
#[derive(Debug, Clone)] ///
/// [`BasicAuth`]: ./struct.BasicAuth.html
/// [`WWW-Authenticate`]: ../../headers/www_authenticate/struct.WwwAuthenticate.html
#[derive(Debug, Clone, Default)]
pub struct Config(Challenge); pub struct Config(Challenge);
impl Config { impl Config {
@ -18,12 +24,21 @@ impl Config {
/// ///
/// The "realm" attribute indicates the scope of protection in the manner described in HTTP/1.1 /// 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). /// [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2).
pub fn realm<T: Into<String>>(&mut self, value: T) -> &mut Config { pub fn realm<T>(mut self, value: T) -> Config
where
T: Into<Cow<'static, str>>,
{
self.0.realm = Some(value.into()); self.0.realm = Some(value.into());
self self
} }
} }
impl AsRef<Challenge> for Config {
fn as_ref(&self) -> &Challenge {
&self.0
}
}
impl ExtractorConfig for Config { impl ExtractorConfig for Config {
type Inner = Challenge; type Inner = Challenge;
@ -32,49 +47,94 @@ impl ExtractorConfig for Config {
} }
} }
impl Default for Config { /// Extractor for HTTP Basic auth.
fn default() -> Self {
Config(Challenge::default())
}
}
/// Extractor for HTTP Basic auth
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// # extern crate actix_web;
/// # extern crate actix_web_httpauth;
/// use actix_web::Result; /// use actix_web::Result;
/// use actix_web_httpauth::extractors::basic::BasicAuth; /// use actix_web_httpauth::extractors::basic::BasicAuth;
/// ///
/// fn index(auth: BasicAuth) -> Result<String> { /// fn index(auth: BasicAuth) -> String {
/// Ok(format!("Hello, {}!", auth.username())) /// format!("Hello, {}!", auth.user_id())
/// } /// }
/// ``` /// ```
///
/// If authentication fails, this extractor fetches the [`Config`] instance
/// from the [app data] in order to properly form the `WWW-Authenticate` response header.
///
/// ## Example
///
/// ```rust
/// use actix_web::{web, App};
/// use actix_web_httpauth::extractors::basic::{BasicAuth, Config};
///
/// fn index(auth: BasicAuth) -> String {
/// format!("Hello, {}!", auth.user_id())
/// }
///
/// fn main() {
/// let app = App::new()
/// .data(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
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BasicAuth(Basic); pub struct BasicAuth(Basic);
impl BasicAuth { impl BasicAuth {
pub fn username(&self) -> &str { /// Returns client's user-ID.
self.0.username.as_str() pub fn user_id(&self) -> &Cow<'static, str> {
&self.0.user_id()
} }
pub fn password(&self) -> Option<&str> { /// Returns client's password.
match self.0.password { pub fn password(&self) -> Option<&Cow<'static, str>> {
None => None, self.0.password()
Some(ref pwd) => Some(pwd.as_str())
} }
/// 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<S> FromRequest<S> for BasicAuth { impl FromRequest for BasicAuth {
type Future = Result<Self, Self::Error>;
type Config = Config; type Config = Config;
type Result = Result<Self, AuthenticationError<Challenge>>; type Error = AuthenticationError<Challenge>;
fn from_request(req: &HttpRequest<S>, cfg: &<Self as FromRequest<S>>::Config) -> <Self as FromRequest<S>>::Result { fn from_request(req: &HttpRequest, _: &mut Payload) -> <Self as FromRequest>::Future {
Authorization::<Basic>::parse(req) Authorization::<Basic>::parse(req)
.map(|auth| BasicAuth(auth.into_inner())) .map(|auth| BasicAuth(auth.into_scheme()))
.map_err(|_| AuthenticationError::new(cfg.0.clone())) .map_err(|_| {
// TODO: debug! the original error
let challenge = req
.get_app_data::<Self::Config>()
.map(|config| config.0.clone())
// TODO: Add trace! about `Default::default` call
.unwrap_or_else(Default::default);
AuthenticationError::new(challenge)
})
} }
} }

View File

@ -1,16 +1,20 @@
//! Extractor for the "Bearer" HTTP Authentication Scheme
use std::borrow::Cow;
use std::default::Default; use std::default::Default;
use actix_web::{HttpRequest, FromRequest}; use actix_web::dev::{Payload, ServiceRequest};
use actix_web::http::header::Header; use actix_web::http::header::Header;
use actix_web::{FromRequest, HttpRequest};
use headers::authorization;
use headers::www_authenticate::bearer;
pub use headers::www_authenticate::bearer::Error;
use super::errors::AuthenticationError;
use super::config::ExtractorConfig; use super::config::ExtractorConfig;
use super::errors::AuthenticationError;
use crate::headers::authorization;
use crate::headers::www_authenticate::bearer;
pub use crate::headers::www_authenticate::bearer::Error;
/// [BearerAuth](./struct/BearerAuth.html) extractor configuration. /// [BearerAuth](./struct/BearerAuth.html) extractor configuration.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct Config(bearer::Bearer); pub struct Config(bearer::Bearer);
impl Config { impl Config {
@ -18,7 +22,7 @@ impl Config {
/// ///
/// The `"scope"` attribute is a space-delimited list of case-sensitive scope values /// 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. /// indicating the required scope of the access token for accessing the requested resource.
pub fn scope<T: Into<String>>(&mut self, value: T) -> &mut Config { pub fn scope<T: Into<Cow<'static, str>>>(mut self, value: T) -> Config {
self.0.scope = Some(value.into()); self.0.scope = Some(value.into());
self self
} }
@ -27,12 +31,18 @@ impl Config {
/// ///
/// The "realm" attribute indicates the scope of protection in the manner described in HTTP/1.1 /// 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). /// [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2).
pub fn realm<T: Into<String>>(&mut self, value: T) -> &mut Config { pub fn realm<T: Into<Cow<'static, str>>>(mut self, value: T) -> Config {
self.0.realm = Some(value.into()); self.0.realm = Some(value.into());
self self
} }
} }
impl AsRef<bearer::Bearer> for Config {
fn as_ref(&self) -> &bearer::Bearer {
&self.0
}
}
impl ExtractorConfig for Config { impl ExtractorConfig for Config {
type Inner = bearer::Bearer; type Inner = bearer::Bearer;
@ -41,60 +51,114 @@ impl ExtractorConfig for Config {
} }
} }
impl Default for Config {
fn default() -> Self {
Config(bearer::Bearer::default())
}
}
/// Extractor for HTTP Bearer auth /// Extractor for HTTP Bearer auth
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// # extern crate actix_web;
/// # extern crate actix_web_httpauth;
/// use actix_web::Result;
/// use actix_web_httpauth::extractors::bearer::BearerAuth; /// use actix_web_httpauth::extractors::bearer::BearerAuth;
/// ///
/// fn index(auth: BearerAuth) -> Result<String> { /// fn index(auth: BearerAuth) -> String {
/// Ok(format!("Hello, user with token {}!", auth.token())) /// format!("Hello, user with token {}!", auth.token())
/// }
/// ```
///
/// If authentication fails, this extractor fetches the [`Config`] instance
/// from the [app data] in order to properly form the `WWW-Authenticate` response header.
///
/// ## Example
///
/// ```rust
/// use actix_web::{web, App};
/// use actix_web_httpauth::extractors::bearer::{BearerAuth, Config};
///
/// fn index(auth: BearerAuth) -> String {
/// format!("Hello, {}!", auth.token())
/// }
///
/// fn main() {
/// let app = App::new()
/// .data(Config::default().realm("Restricted area").scope("email photo"))
/// .service(web::resource("/index.html").route(web::get().to(index)));
/// } /// }
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BearerAuth(authorization::Bearer); pub struct BearerAuth(authorization::Bearer);
impl BearerAuth { impl BearerAuth {
/// Returns bearer token provided by client.
pub fn token(&self) -> &str { pub fn token(&self) -> &str {
self.0.token.as_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<S> FromRequest<S> for BearerAuth { impl FromRequest for BearerAuth {
type Config = Config; type Config = Config;
type Result = Result<Self, AuthenticationError<bearer::Bearer>>; type Future = Result<Self, Self::Error>;
type Error = AuthenticationError<bearer::Bearer>;
fn from_request(req: &HttpRequest<S>, cfg: &<Self as FromRequest<S>>::Config) -> <Self as FromRequest<S>>::Result { fn from_request(req: &HttpRequest, _payload: &mut Payload) -> <Self as FromRequest>::Future {
authorization::Authorization::<authorization::Bearer>::parse(req) authorization::Authorization::<authorization::Bearer>::parse(req)
.map(|auth| BearerAuth(auth.into_inner())) .map(|auth| BearerAuth(auth.into_scheme()))
.map_err(|_| AuthenticationError::new(cfg.0.clone())) .map_err(|_| {
let bearer = req
.app_data::<Self::Config>()
.map(|config| config.0.clone())
.unwrap_or_else(Default::default);
AuthenticationError::new(bearer)
})
} }
} }
/// Extended error customization for HTTP `Bearer` auth. /// Extended error customization for HTTP `Bearer` auth.
impl AuthenticationError<bearer::Bearer> { 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.
pub fn with_error(mut self, kind: Error) -> Self { pub fn with_error(mut self, kind: Error) -> Self {
*self.status_code_mut() = kind.status_code(); *self.status_code_mut() = kind.status_code();
self.challenge_mut().error = Some(kind); self.challenge_mut().error = Some(kind);
self self
} }
pub fn with_error_description<T: Into<String>>(mut self, desc: T) -> Self { /// Attach error description to the current Authentication error.
pub fn with_error_description<T>(mut self, desc: T) -> Self
where
T: Into<Cow<'static, str>>,
{
self.challenge_mut().error_description = Some(desc.into()); self.challenge_mut().error_description = Some(desc.into());
self self
} }
pub fn with_error_uri<T: Into<String>>(mut self, uri: T) -> Self { /// Attach error URI to the current Authentication error.
///
/// It is up to implementor to provide properly formed absolute URI.
pub fn with_error_uri<T>(mut self, uri: T) -> Self
where
T: Into<Cow<'static, str>>,
{
self.challenge_mut().error_uri = Some(uri.into()); self.challenge_mut().error_uri = Some(uri.into());
self self
} }

View File

@ -1,6 +1,5 @@
use headers::www_authenticate::Challenge;
use super::AuthenticationError; use super::AuthenticationError;
use crate::headers::www_authenticate::Challenge;
pub trait ExtractorConfig { pub trait ExtractorConfig {
type Inner: Challenge; type Inner: Challenge;
@ -8,7 +7,10 @@ pub trait ExtractorConfig {
fn into_inner(self) -> Self::Inner; fn into_inner(self) -> Self::Inner;
} }
impl<T> From<T> for AuthenticationError<<T as ExtractorConfig>::Inner> where T: ExtractorConfig { impl<T> From<T> for AuthenticationError<<T as ExtractorConfig>::Inner>
where
T: ExtractorConfig,
{
fn from(config: T) -> Self { fn from(config: T) -> Self {
AuthenticationError::new(config.into_inner()) AuthenticationError::new(config.into_inner())
} }

View File

@ -1,14 +1,13 @@
use std::str;
use std::fmt;
use std::error::Error; use std::error::Error;
use std::fmt;
use actix_web::{HttpResponse, ResponseError};
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::{HttpResponse, ResponseError};
use headers::www_authenticate::{WWWAuthenticate}; use crate::headers::www_authenticate::Challenge;
use headers::www_authenticate::Challenge; use crate::headers::www_authenticate::WwwAuthenticate;
/// Authentication error returned by Auth extractor. /// Authentication error returned by authentication extractors.
/// ///
/// Different extractors may extend `AuthenticationError` implementation /// Different extractors may extend `AuthenticationError` implementation
/// in order to provide access to inner challenge fields. /// in order to provide access to inner challenge fields.
@ -19,6 +18,9 @@ pub struct AuthenticationError<C: Challenge> {
} }
impl<C: Challenge> AuthenticationError<C> { impl<C: Challenge> AuthenticationError<C> {
/// Creates new authentication error from the provided `challenge`.
///
/// By default returned error will resolve into the `HTTP 401` status code.
pub fn new(challenge: C) -> AuthenticationError<C> { pub fn new(challenge: C) -> AuthenticationError<C> {
AuthenticationError { AuthenticationError {
challenge, challenge,
@ -26,10 +28,15 @@ impl<C: Challenge> AuthenticationError<C> {
} }
} }
/// Returns mutable reference to the inner challenge instance.
pub fn challenge_mut(&mut self) -> &mut C { pub fn challenge_mut(&mut self) -> &mut C {
&mut self.challenge &mut self.challenge
} }
/// 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.
pub fn status_code_mut(&mut self) -> &mut StatusCode { pub fn status_code_mut(&mut self) -> &mut StatusCode {
&mut self.status_code &mut self.status_code
} }
@ -37,27 +44,17 @@ impl<C: Challenge> AuthenticationError<C> {
impl<C: Challenge> fmt::Display for AuthenticationError<C> { impl<C: Challenge> fmt::Display for AuthenticationError<C> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let bytes = self.challenge.to_bytes(); fmt::Display::fmt(&self.status_code, f)
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<C: 'static + Challenge> Error for AuthenticationError<C> { impl<C: 'static + Challenge> Error for AuthenticationError<C> {}
fn description(&self) -> &str {
unimplemented!()
}
}
impl<C: 'static + Challenge> ResponseError for AuthenticationError<C> { impl<C: 'static + Challenge> ResponseError for AuthenticationError<C> {
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code) HttpResponse::build(self.status_code)
// TODO: Get rid of the `.clone()` // TODO: Get rid of the `.clone()`
.set(WWWAuthenticate(self.challenge.clone())) .set(WwwAuthenticate(self.challenge.clone()))
.finish() .finish()
} }
} }

View File

@ -1,6 +1,8 @@
mod errors; //! Type-safe authentication information extractors
mod config;
pub mod basic; pub mod basic;
pub mod bearer; pub mod bearer;
mod config;
mod errors;
pub use self::errors::AuthenticationError; pub use self::errors::AuthenticationError;

View File

@ -1,9 +1,8 @@
use std::str;
use std::fmt;
use std::error::Error;
use std::convert::From; use std::convert::From;
use std::error::Error;
use std::fmt;
use std::str;
use base64;
use actix_web::http::header; use actix_web::http::header;
/// Possible errors while parsing `Authorization` header. /// Possible errors while parsing `Authorization` header.
@ -18,8 +17,11 @@ pub enum ParseError {
MissingScheme, MissingScheme,
/// Required authentication field is missing /// Required authentication field is missing
MissingField(&'static str), MissingField(&'static str),
/// Unable to convert header into the str
ToStrError(header::ToStrError), ToStrError(header::ToStrError),
/// Malformed base64 string
Base64DecodeError(base64::DecodeError), Base64DecodeError(base64::DecodeError),
/// Malformed UTF-8 string
Utf8Error(str::Utf8Error), Utf8Error(str::Utf8Error),
} }
@ -41,7 +43,7 @@ impl Error for ParseError {
} }
} }
fn cause(&self) -> Option<&Error> { fn source(&self) -> Option<&(dyn Error + 'static)> {
match self { match self {
ParseError::Invalid => None, ParseError::Invalid => None,
ParseError::MissingScheme => None, ParseError::MissingScheme => None,

View File

@ -1,12 +1,10 @@
use std::ops;
use std::fmt; use std::fmt;
use actix_web::{HttpMessage};
use actix_web::error::ParseError; 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 headers::authorization::scheme::Scheme; use crate::headers::authorization::scheme::Scheme;
/// `Authorization` header, defined in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.2) /// `Authorization` header, defined in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.2)
/// ///
@ -21,27 +19,57 @@ use headers::authorization::scheme::Scheme;
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// # extern crate actix_web; /// # use actix_web::http::header::Header;
/// # extern crate actix_web_httpauth; /// # use actix_web::{HttpRequest, Result};
/// /// # use actix_web_httpauth::headers::authorization::{Authorization, Basic};
/// use actix_web::{HttpRequest, Result};
/// use actix_web::http::header::Header;
/// use actix_web_httpauth::headers::authorization::{Authorization, Basic};
///
/// fn handler(req: HttpRequest) -> Result<String> { /// fn handler(req: HttpRequest) -> Result<String> {
/// let auth = Authorization::<Basic>::parse(&req)?; /// let auth = Authorization::<Basic>::parse(&req)?;
/// ///
/// Ok(format!("Hello, {}!", auth.username)) /// Ok(format!("Hello, {}!", auth.as_ref().user_id()))
/// } /// }
/// ``` /// ```
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
pub struct Authorization<S: Scheme>(S); pub struct Authorization<S: Scheme>(S);
impl<S: Scheme> Authorization<S> { impl<S> Authorization<S>
pub fn into_inner(self) -> S { where
S: Scheme,
{
/// Consumes `Authorization` header and returns inner [`Scheme`] implementation.
///
/// [`Scheme`]: ./trait.Scheme.html
pub fn into_scheme(self) -> S {
self.0 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> { impl<S: Scheme> Header for Authorization<S> {
#[inline] #[inline]
fn name() -> HeaderName { fn name() -> HeaderName {
@ -69,17 +97,3 @@ impl<S: Scheme> fmt::Display for Authorization<S> {
fmt::Display::fmt(&self.0, f) fmt::Display::fmt(&self.0, f)
} }
} }
impl<S: Scheme> ops::Deref for Authorization<S> {
type Target = S;
fn deref(&self) -> &<Self as ops::Deref>::Target {
&self.0
}
}
impl<S: Scheme> ops::DerefMut for Authorization<S> {
fn deref_mut(&mut self) -> &mut <Self as ops::Deref>::Target {
&mut self.0
}
}

View File

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

View File

@ -1,18 +1,51 @@
use std::str; use std::borrow::Cow;
use std::fmt; use std::fmt;
use std::str;
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes};
use base64; use base64;
use bytes::{BufMut, BytesMut}; use bytes::{BufMut, BytesMut};
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes};
use headers::authorization::Scheme; use crate::headers::authorization::errors::ParseError;
use headers::authorization::errors::ParseError; use crate::headers::authorization::Scheme;
use crate::utils;
/// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617) /// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617)
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Basic { pub struct Basic {
pub username: String, user_id: Cow<'static, str>,
pub password: Option<String>, password: Option<Cow<'static, str>>,
}
impl Basic {
/// Creates `Basic` credentials with provided `user_id` and optional `password`.
///
/// ## Example
///
/// ```rust
/// # 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 { impl Scheme for Basic {
@ -29,24 +62,25 @@ impl Scheme for Basic {
} }
let decoded = base64::decode(parts.next().ok_or(ParseError::Invalid)?)?; let decoded = base64::decode(parts.next().ok_or(ParseError::Invalid)?)?;
let mut credentials = str::from_utf8(&decoded)? let mut credentials = str::from_utf8(&decoded)?.splitn(2, ':');
.splitn(2, ':');
let username = credentials.next() let user_id = credentials
.ok_or(ParseError::MissingField("username")) .next()
.map(|username| username.to_string())?; .ok_or(ParseError::MissingField("user_id"))
let password = credentials.next() .map(|user_id| user_id.to_string().into())?;
let password = credentials
.next()
.ok_or(ParseError::MissingField("password")) .ok_or(ParseError::MissingField("password"))
.map(|password| { .map(|password| {
if password.is_empty() { if password.is_empty() {
None None
} else { } else {
Some(password.to_string()) Some(password.to_string().into())
} }
})?; })?;
Ok(Basic { Ok(Basic {
username, user_id,
password, password,
}) })
} }
@ -54,14 +88,13 @@ impl Scheme for Basic {
impl fmt::Debug for Basic { impl fmt::Debug for Basic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_fmt(format_args!("Basic {}:******", self.username)) f.write_fmt(format_args!("Basic {}:******", self.user_id))
} }
} }
impl fmt::Display for Basic { impl fmt::Display for Basic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO: Display password also f.write_fmt(format_args!("Basic {}:******", self.user_id))
f.write_fmt(format_args!("Basic {}:******", self.username))
} }
} }
@ -69,13 +102,12 @@ impl IntoHeaderValue for Basic {
type Error = InvalidHeaderValueBytes; type Error = InvalidHeaderValueBytes;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> { fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
let mut credentials = BytesMut::with_capacity( let mut credentials =
self.username.len() + 1 + self.password.as_ref().map_or(0, |pwd| pwd.len()) 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(&self.username);
credentials.put_u8(b':'); credentials.put_u8(b':');
if let Some(ref password) = self.password { if let Some(ref password) = self.password {
credentials.put(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`
@ -90,8 +122,8 @@ impl IntoHeaderValue for Basic {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{Basic, Scheme};
use actix_web::http::header::{HeaderValue, IntoHeaderValue}; use actix_web::http::header::{HeaderValue, IntoHeaderValue};
use super::{Scheme, Basic};
#[test] #[test]
fn test_parse_header() { fn test_parse_header() {
@ -100,8 +132,8 @@ mod tests {
assert!(scheme.is_ok()); assert!(scheme.is_ok());
let scheme = scheme.unwrap(); let scheme = scheme.unwrap();
assert_eq!(scheme.username, "Aladdin"); assert_eq!(scheme.user_id, "Aladdin");
assert_eq!(scheme.password, Some("open sesame".to_string())); assert_eq!(scheme.password, Some("open sesame".into()));
} }
#[test] #[test]
@ -111,7 +143,7 @@ mod tests {
assert!(scheme.is_ok()); assert!(scheme.is_ok());
let scheme = scheme.unwrap(); let scheme = scheme.unwrap();
assert_eq!(scheme.username, "Aladdin"); assert_eq!(scheme.user_id, "Aladdin");
assert_eq!(scheme.password, None); assert_eq!(scheme.password, None);
} }
@ -150,17 +182,19 @@ mod tests {
#[test] #[test]
fn test_into_header_value() { fn test_into_header_value() {
let basic = Basic { let basic = Basic {
username: "Aladdin".to_string(), user_id: "Aladdin".into(),
password: Some("open sesame".to_string()), password: Some("open sesame".into()),
}; };
let result = basic.try_into(); let result = basic.try_into();
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")); assert_eq!(
result.unwrap(),
HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
);
} }
} }
#[cfg(all(test, feature = "nightly"))] #[cfg(all(test, feature = "nightly"))]
mod benches { mod benches {
use test::Bencher; use test::Bencher;
@ -172,17 +206,15 @@ mod benches {
#[bench] #[bench]
fn bench_parsing(b: &mut Bencher) { fn bench_parsing(b: &mut Bencher) {
let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
b.iter(|| { b.iter(|| Basic::parse(&value));
Basic::parse(&value)
});
} }
#[bench] #[bench]
fn bench_serializing(b: &mut Bencher) { fn bench_serializing(b: &mut Bencher) {
b.iter(|| { b.iter(|| {
let basic = Basic { let basic = Basic {
username: "Aladdin".to_string(), user_id: "Aladdin".into(),
password: Some("open sesame".to_string()), password: Some("open sesame".into()),
}; };
basic.try_into() basic.try_into()

View File

@ -1,17 +1,43 @@
use std::borrow::Cow;
use std::fmt; use std::fmt;
use bytes::{BufMut, BytesMut};
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes};
use bytes::{BufMut, BytesMut};
use headers::authorization::scheme::Scheme; use crate::headers::authorization::errors::ParseError;
use headers::authorization::errors::ParseError; use crate::headers::authorization::scheme::Scheme;
use crate::utils;
/// Credentials for `Bearer` authentication scheme, defined in [RFC6750](https://tools.ietf.org/html/rfc6750) /// 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)] #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Bearer { pub struct Bearer {
pub token: String, token: Cow<'static, str>,
}
impl Bearer {
/// Creates new `Bearer` credentials with the token provided.
///
/// ## Example
///
/// ```rust
/// # 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 { impl Scheme for Bearer {
@ -30,7 +56,7 @@ impl Scheme for Bearer {
let token = parts.next().ok_or(ParseError::Invalid)?; let token = parts.next().ok_or(ParseError::Invalid)?;
Ok(Bearer { Ok(Bearer {
token: token.to_string(), token: token.to_string().into(),
}) })
} }
} }
@ -53,7 +79,7 @@ impl IntoHeaderValue for Bearer {
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> { fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
let mut buffer = BytesMut::with_capacity(7 + self.token.len()); let mut buffer = BytesMut::with_capacity(7 + self.token.len());
buffer.put("Bearer "); buffer.put("Bearer ");
buffer.put(self.token); utils::put_cow(&mut buffer, &self.token);
HeaderValue::from_shared(buffer.freeze()) HeaderValue::from_shared(buffer.freeze())
} }
@ -61,8 +87,8 @@ impl IntoHeaderValue for Bearer {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{Bearer, Scheme};
use actix_web::http::header::{HeaderValue, IntoHeaderValue}; use actix_web::http::header::{HeaderValue, IntoHeaderValue};
use super::{Scheme, Bearer};
#[test] #[test]
fn test_parse_header() { fn test_parse_header() {
@ -100,9 +126,7 @@ mod tests {
#[test] #[test]
fn test_into_header_value() { fn test_into_header_value() {
let bearer = Bearer { let bearer = Bearer::new("mF_9.B5f-4.1JqM");
token: "mF_9.B5f-4.1JqM".to_string(),
};
let result = bearer.try_into(); let result = bearer.try_into();
assert!(result.is_ok()); assert!(result.is_ok());

View File

@ -1,13 +1,14 @@
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};
use actix_web::http::header::{IntoHeaderValue, HeaderValue}; use actix_web::http::header::{HeaderValue, IntoHeaderValue};
pub mod basic; pub mod basic;
pub mod bearer; pub mod bearer;
use headers::authorization::errors::ParseError; use crate::headers::authorization::errors::ParseError;
/// Authentication scheme for [`Authorization`](./struct.Authorization.html) header. /// Authentication scheme for [`Authorization`](./struct.Authorization.html) header.
pub trait Scheme: IntoHeaderValue + Debug + Display + Clone + Send + Sync { 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>; fn parse(header: &HeaderValue) -> Result<Self, ParseError>;
} }

View File

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

View File

@ -1,20 +1,78 @@
use std::str; //! Challenge for the "Basic" HTTP Authentication Scheme
use std::fmt;
use std::default::Default; use std::borrow::Cow;
use std::default::Default;
use std::fmt;
use std::str;
use bytes::{BufMut, Bytes, BytesMut};
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes};
use bytes::{BufMut, Bytes, BytesMut};
use super::Challenge; use super::Challenge;
use crate::utils;
/// Challenge for `WWW-Authenticate` header with HTTP Basic auth scheme, /// Challenge for [`WWW-Authenticate`] header with HTTP Basic auth scheme,
/// described in [RFC 7617](https://tools.ietf.org/html/rfc7617) /// described in [RFC 7617](https://tools.ietf.org/html/rfc7617)
#[derive(Debug, Clone)] ///
/// ## Example
///
/// ```rust
/// # 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 { pub struct Basic {
// "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A // "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A
pub realm: Option<String>, pub(crate) realm: Option<Cow<'static, str>>,
} }
impl Basic {
/// Creates new `Basic` challenge with an empty `realm` field.
///
/// ## Example
///
/// ```rust
/// # 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
///
/// ```rust
/// # use actix_web_httpauth::headers::www_authenticate::basic::Basic;
/// let challenge = Basic::with_realm("Restricted area");
/// ```
///
/// ```rust
/// # 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 { impl Challenge for Basic {
fn to_bytes(&self) -> Bytes { fn to_bytes(&self) -> Bytes {
// 5 is for `"Basic"`, 9 is for `"realm=\"\""` // 5 is for `"Basic"`, 9 is for `"realm=\"\""`
@ -23,7 +81,7 @@ impl Challenge for Basic {
buffer.put("Basic"); buffer.put("Basic");
if let Some(ref realm) = self.realm { if let Some(ref realm) = self.realm {
buffer.put(" realm=\""); buffer.put(" realm=\"");
buffer.put(realm); utils::put_cow(&mut buffer, realm);
buffer.put("\""); buffer.put("\"");
} }
@ -51,15 +109,6 @@ impl IntoHeaderValue for Basic {
} }
} }
impl Default for Basic {
fn default() -> Self {
Self {
realm: None,
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Basic; use super::Basic;
@ -80,7 +129,7 @@ mod tests {
#[test] #[test]
fn test_with_realm_into_header_value() { fn test_with_realm_into_header_value() {
let challenge = Basic { let challenge = Basic {
realm: Some("Restricted area".to_string()), realm: Some("Restricted area".into()),
}; };
let value = challenge.try_into(); let value = challenge.try_into();

View File

@ -1,156 +0,0 @@
use std::str;
use std::fmt;
use std::default::Default;
use bytes::{BufMut, Bytes, BytesMut};
use actix_web::http::StatusCode;
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes};
use super::Challenge;
/// Bearer authorization error types, described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3.1)
#[derive(Debug, Copy, Clone)]
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 {
pub fn status_code(&self) -> StatusCode {
match *self {
Error::InvalidRequest => StatusCode::BAD_REQUEST,
Error::InvalidToken => StatusCode::UNAUTHORIZED,
Error::InsufficientScope => StatusCode::FORBIDDEN,
}
}
fn as_str(&self) -> &'static str {
match *self {
Error::InvalidRequest => "invalid_request",
Error::InvalidToken => "invalid_token",
Error::InsufficientScope => "insufficient_scope",
}
}
}
/// Challenge for `WWW-Authenticate` header with HTTP Bearer auth scheme,
/// described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3)
#[derive(Debug, Clone)]
pub struct Bearer {
pub scope: Option<String>,
pub realm: Option<String>,
pub error: Option<Error>,
pub error_description: Option<String>,
/// It is up to implementor to provide correct absolute URI
pub error_uri: Option<String>,
}
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("Bearer");
if let Some(ref realm) = self.realm {
buffer.put(" realm=\"");
buffer.put(realm);
buffer.put("\"");
}
if let Some(ref scope) = self.scope {
buffer.put(" scope=\"");
buffer.put(scope);
buffer.put("\"");
}
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(" error=\"");
buffer.put(error_repr);
buffer.put("\"")
}
if let Some(ref error_description) = self.error_description {
buffer.put(" error_description=\"");
buffer.put(error_description);
buffer.put("\"");
}
if let Some(ref error_uri) = self.error_uri {
buffer.put(" error_uri=\"");
buffer.put(error_uri);
buffer.put("\"");
}
buffer.freeze()
}
}
impl Default for Bearer {
fn default() -> Self {
Bearer {
scope: None,
realm: None,
error: None,
error_description: None,
error_uri: None,
}
}
}
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 = InvalidHeaderValueBytes;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
HeaderValue::from_shared(self.to_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_bytes() {
let b = Bearer {
scope: None,
realm: None,
error: Some(Error::InvalidToken),
error_description: Some(String::from("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found")),
error_uri: None,
};
assert_eq!("Bearer error=\"invalid_token\" error_description=\"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found\"",
format!("{}", b));
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,132 @@
use std::borrow::Cow;
use std::fmt;
use std::str;
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes};
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
///
/// ```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::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
///
/// ```rust
/// # 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("Bearer");
if let Some(ref realm) = self.realm {
buffer.put(" realm=\"");
utils::put_cow(&mut buffer, realm);
buffer.put("\"");
}
if let Some(ref scope) = self.scope {
buffer.put(" scope=\"");
utils::put_cow(&mut buffer, scope);
buffer.put("\"");
}
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(" error=\"");
buffer.put(error_repr);
buffer.put("\"")
}
if let Some(ref error_description) = self.error_description {
buffer.put(" error_description=\"");
utils::put_cow(&mut buffer, error_description);
buffer.put("\"");
}
if let Some(ref error_uri) = self.error_uri {
buffer.put(" error_uri=\"");
utils::put_cow(&mut buffer, error_uri);
buffer.put("\"");
}
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 = InvalidHeaderValueBytes;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
HeaderValue::from_shared(self.to_bytes())
}
}

View File

@ -0,0 +1,48 @@
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())
}
}

View File

@ -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;

View File

@ -0,0 +1,14 @@
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,12 +1,13 @@
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};
use bytes::Bytes;
use actix_web::http::header::IntoHeaderValue; use actix_web::http::header::IntoHeaderValue;
use bytes::Bytes;
pub mod basic; pub mod basic;
pub mod bearer; pub mod bearer;
/// Authentication challenge for `WWW-Authenticate` header. /// 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; fn to_bytes(&self) -> Bytes;
} }

View File

@ -1,36 +1,18 @@
use actix_web::{HttpMessage};
use actix_web::error::ParseError; 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; 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](https://tools.ietf.org/html/rfc7235#section-4.1)
/// ///
/// `WWW-Authenticate` header is generic over [Challenge](./trait.Challenge.html) /// This header is generic over [Challenge](./trait.Challenge.html) trait,
/// /// see [Basic](./basic/struct.Basic.html) and [Bearer](./bearer/struct.Bearer.html)
/// # Example /// challenges for details.
/// #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
/// ```rust pub struct WwwAuthenticate<C: Challenge>(pub C);
/// # extern crate actix_web;
/// # extern crate actix_web_httpauth;
///
/// use actix_web::{HttpRequest, HttpResponse};
/// use actix_web::http::StatusCode;
/// use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate};
/// use actix_web_httpauth::headers::www_authenticate::basic::Basic;
///
/// fn handler(req: HttpRequest) -> HttpResponse {
/// let challenge = Basic {
/// realm: Some("Restricted area".to_string()),
/// };
/// req.build_response(StatusCode::UNAUTHORIZED)
/// .set(WWWAuthenticate(challenge))
/// .finish()
/// }
/// ```
pub struct WWWAuthenticate<C: Challenge>(pub C);
impl<C: Challenge> Header for WWWAuthenticate<C> { impl<C: Challenge> Header for WwwAuthenticate<C> {
fn name() -> HeaderName { fn name() -> HeaderName {
WWW_AUTHENTICATE WWW_AUTHENTICATE
} }
@ -40,7 +22,7 @@ impl<C: Challenge> Header for WWWAuthenticate<C> {
} }
} }
impl<C: Challenge> IntoHeaderValue for WWWAuthenticate<C> { impl<C: Challenge> IntoHeaderValue for WwwAuthenticate<C> {
type Error = <C as IntoHeaderValue>::Error; type Error = <C as IntoHeaderValue>::Error;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> { fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {

View File

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

View File

@ -1,15 +1,21 @@
//! HTTP Authorization support for [actix-web](https://actix.rs) framework. //! HTTP Authorization support for [actix-web](https://actix.rs) framework.
//! //!
//! Provides [`Authorization`](./headers/authorization/struct.Authorization.html) //! Provides [Authorization] and [WWW-Authenticate] headers,
//! and [`WWW-Authenticate`](./headers/www_authenticate/struct.WWWAuthenticate.html) headers, //! and [extractors] for an [Authorization] header.
//! and `actix-web` extractors for an `Authorization` header. //!
//! ## Supported schemes
//!
//! * `Basic`, as defined in [RFC7617](https://tools.ietf.org/html/rfc7617)
//! * `Bearer`, as defined in [RFC6750](https://tools.ietf.org/html/rfc6750)
//!
//! [Authorization]: `crate::headers::authorization::Authorization`
//! [WWW-Authenticate]: `crate::headers::www_authenticate::WwwAuthenticate`
//! [extractors]: https://actix.rs/docs/extractors/
#![forbid(bare_trait_objects)]
#![forbid(missing_docs)]
#![cfg_attr(feature = "nightly", feature(test))] #![cfg_attr(feature = "nightly", feature(test))]
#[cfg(feature = "nightly")] extern crate test;
extern crate actix_web;
extern crate bytes;
extern crate base64;
pub mod headers;
pub mod extractors; pub mod extractors;
pub mod headers;
mod utils;

13
src/utils.rs Normal file
View File

@ -0,0 +1,13 @@
use std::borrow::Cow;
use bytes::{BufMut, BytesMut};
// `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>) {
match value {
Cow::Borrowed(str) => buf.put(str),
Cow::Owned(ref string) => buf.put(string),
}
}