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

Merge branch 'httpauth'

This commit is contained in:
Rob Ede 2020-01-29 12:02:17 +00:00
commit e1f99b0b9a
No known key found for this signature in database
GPG Key ID: C2A3B36E841A91E6
37 changed files with 2437 additions and 0 deletions

View File

@ -0,0 +1,15 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.yml]
indent_size = 2
[*.md]
indent_size = 2

View File

@ -0,0 +1,28 @@
name: Clippy and rustfmt check
on:
push:
branches:
- master
pull_request:
jobs:
clippy_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
components: clippy, rustfmt
override: true
- name: Clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-features
- name: rustfmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check

View File

@ -0,0 +1,83 @@
name: CI
on:
push:
branches:
- master
pull_request:
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
toolchain:
- x86_64-pc-windows-msvc
- x86_64-pc-windows-gnu
- x86_64-unknown-linux-gnu
- x86_64-apple-darwin
version:
- stable
- nightly
include:
- toolchain: x86_64-pc-windows-msvc
os: windows-latest
- toolchain: x86_64-pc-windows-gnu
os: windows-latest
- toolchain: x86_64-unknown-linux-gnu
os: ubuntu-latest
- toolchain: x86_64-apple-darwin
os: macOS-latest
name: ${{ matrix.version }} - ${{ matrix.toolchain }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@master
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.toolchain }}
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: update
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ matrix.version }}-${{ matrix.toolchain }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ matrix.version }}-${{ matrix.toolchain }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ matrix.version }}-${{ matrix.toolchain }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
- name: checks
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests (stable)
if: matrix.version == 'stable'
uses: actions-rs/cargo@v1
with:
command: test
args: --all --no-fail-fast -- --nocapture
- name: tests (nightly)
if: matrix.version == 'nightly'
uses: actions-rs/cargo@v1
with:
command: test
args: --all --all-features --no-fail-fast -- --nocapture

View File

@ -0,0 +1,55 @@
name: Check MSRV
on:
push:
branches:
- master
pull_request:
jobs:
build_and_test:
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: 1.39.0-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: update
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: msrv-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: msrv-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: msrv-cargo-build-${{ hashFiles('**/Cargo.lock') }}
- name: checks
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all --no-fail-fast -- --nocapture

3
actix-web-httpauth/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

View File

@ -0,0 +1,49 @@
# Changelog
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/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [0.4.0] - 2020-01-14
### Changed
- Depends on `actix-web = "^2.0"`, `actix-service = "^1.0"`, and `futures = "^0.3"` version now ([#14])
- Depends on `bytes = "^0.5"` and `base64 = "^0.11"` now
[#14]: https://github.com/actix/actix-web-httpauth/pull/14
## [0.3.2] - 2019-07-19
### Changed
- Middleware accepts any `Fn` as a validator function instead of `FnMut` ([#11](https://github.com/actix/actix-web-httpauth/pull/11))
## [0.3.1] - 2019-06-09
### Fixed
- Multiple calls to the middleware would result in panic
## [0.3.0] - 2019-06-05
### Changed
- 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
### Changed
- `actix-web` dependency is used without default features now ([#6](https://github.com/actix/actix-web-httpauth/pull/6))
- `base64` dependency version was bumped to `0.10`
## [0.1.0] - 2018-09-08
### Changed
- Update to `actix-web = "0.7"` version
## [0.0.4] - 2018-07-01
### Fixed
- Fix possible panic at `IntoHeaderValue` implementation for `headers::authorization::Basic`
- Fix possible panic at `headers::www_authenticate::challenge::bearer::Bearer::to_bytes` call

View File

@ -0,0 +1,31 @@
[package]
name = "actix-web-httpauth"
version = "0.4.0"
authors = ["svartalf <self@svartalf.info>", "Yuki Okushi <huyuumi.dev@gmail.com>"]
description = "HTTP authentication schemes for actix-web"
readme = "README.md"
keywords = ["http", "web", "framework"]
homepage = "https://github.com/actix/actix-web-httpauth"
repository = "https://github.com/actix/actix-web-httpauth.git"
documentation = "https://docs.rs/actix-web-httpauth/"
categories = ["web-programming::http-server"]
license = "MIT OR Apache-2.0"
exclude = [".github/*", ".gitignore"]
edition = "2018"
[dependencies]
actix-web = { version = "^2.0", default_features = false }
actix-service = "1.0"
futures = "0.3"
bytes = "0.5"
base64 = "0.11"
[dev-dependencies]
actix-rt = "1.0"
[features]
default = []
nightly = []
[badges]
maintenance = { status = "passively-maintained" }

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017-NOW svartalf and Actix team
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,25 @@
Copyright (c) 2017 svartalf and Actix team
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,22 @@
# actix-web-httpauth
[![Latest Version](https://img.shields.io/crates/v/actix-web-httpauth.svg)](https://crates.io/crates/actix-web-httpauth)
[![Latest Version](https://docs.rs/actix-web-httpauth/badge.svg)](https://docs.rs/actix-web-httpauth)
[![dependency status](https://deps.rs/crate/actix-web-httpauth/0.4.0/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.4.0)
![Build Status](https://github.com/actix/actix-web-httpauth/workflows/CI/badge.svg?branch=master&event=push)
![Apache 2.0 OR MIT licensed](https://img.shields.io/badge/license-Apache2.0%2FMIT-blue.svg)
HTTP authentication schemes for [actix-web](https://github.com/actix/actix-web) framework.
Provides:
* typed [Authorization] and [WWW-Authenticate] headers
* [extractors] for an [Authorization] header
* [middleware] for easier authorization checking
All supported schemas are actix [Extractors](https://docs.rs/actix-web/1.0.0/actix_web/trait.FromRequest.html),
and can be used both in the middlewares and request handlers.
## Supported schemes
* [Basic](https://tools.ietf.org/html/rfc7617)
* [Bearer](https://tools.ietf.org/html/rfc6750)

View File

@ -0,0 +1,19 @@
use actix_web::{middleware, web, App, HttpServer};
use actix_web_httpauth::middleware::HttpAuthentication;
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
let auth =
HttpAuthentication::basic(|req, _credentials| async { Ok(req) });
App::new()
.wrap(middleware::Logger::default())
.wrap(auth)
.service(web::resource("/").to(|| async { "Test\r\n" }))
})
.bind("127.0.0.1:8080")?
.workers(1)
.run()
.await
}

View File

@ -0,0 +1,27 @@
use actix_web::dev::ServiceRequest;
use actix_web::{middleware, web, App, Error, HttpServer};
use actix_web_httpauth::extractors::basic::BasicAuth;
use actix_web_httpauth::middleware::HttpAuthentication;
async fn validator(
req: ServiceRequest,
_credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
Ok(req)
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
let auth = HttpAuthentication::basic(validator);
App::new()
.wrap(middleware::Logger::default())
.wrap(auth)
.service(web::resource("/").to(|| async { "Test\r\n" }))
})
.bind("127.0.0.1:8080")?
.workers(1)
.run()
.await
}

View File

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

View File

@ -0,0 +1,152 @@
//! Extractor for the "Basic" HTTP Authentication Scheme
use std::borrow::Cow;
use actix_web::dev::{Payload, ServiceRequest};
use actix_web::http::header::Header;
use actix_web::{FromRequest, HttpRequest};
use futures::future;
use super::config::AuthExtractorConfig;
use super::errors::AuthenticationError;
use super::AuthExtractor;
use crate::headers::authorization::{Authorization, Basic};
use crate::headers::www_authenticate::basic::Basic as Challenge;
/// [`BasicAuth`] extractor configuration,
/// used for [`WWW-Authenticate`] header later.
///
/// [`BasicAuth`]: ./struct.BasicAuth.html
/// [`WWW-Authenticate`]:
/// ../../headers/www_authenticate/struct.WwwAuthenticate.html
#[derive(Debug, Clone, Default)]
pub struct Config(Challenge);
impl Config {
/// Set challenge `realm` attribute.
///
/// The "realm" attribute indicates the scope of protection in the manner
/// described in HTTP/1.1 [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2).
pub fn realm<T>(mut self, value: T) -> Config
where
T: Into<Cow<'static, str>>,
{
self.0.realm = Some(value.into());
self
}
}
impl AsRef<Challenge> for Config {
fn as_ref(&self) -> &Challenge {
&self.0
}
}
impl AuthExtractorConfig for Config {
type Inner = Challenge;
fn into_inner(self) -> Self::Inner {
self.0
}
}
// Needs `fn main` to display complete example.
#[allow(clippy::needless_doctest_main)]
/// Extractor for HTTP Basic auth.
///
/// # Example
///
/// ```
/// use actix_web::Result;
/// use actix_web_httpauth::extractors::basic::BasicAuth;
///
/// async fn index(auth: BasicAuth) -> String {
/// 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
///
/// ```
/// use actix_web::{web, App};
/// use actix_web_httpauth::extractors::basic::{BasicAuth, Config};
///
/// async 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)]
pub struct BasicAuth(Basic);
impl BasicAuth {
/// Returns client's user-ID.
pub fn user_id(&self) -> &Cow<'static, str> {
&self.0.user_id()
}
/// Returns client's password.
pub fn password(&self) -> Option<&Cow<'static, str>> {
self.0.password()
}
}
impl FromRequest for BasicAuth {
type Future = future::Ready<Result<Self, Self::Error>>;
type Config = Config;
type Error = AuthenticationError<Challenge>;
fn from_request(
req: &HttpRequest,
_: &mut Payload,
) -> <Self as FromRequest>::Future {
future::ready(
Authorization::<Basic>::parse(req)
.map(|auth| BasicAuth(auth.into_scheme()))
.map_err(|_| {
// TODO: debug! the original error
let challenge = req
.app_data::<Self::Config>()
.map(|config| config.0.clone())
// TODO: Add trace! about `Default::default` call
.unwrap_or_else(Default::default);
AuthenticationError::new(challenge)
}),
)
}
}
impl AuthExtractor for BasicAuth {
type Error = AuthenticationError<Challenge>;
type Future = future::Ready<Result<Self, Self::Error>>;
fn from_service_request(req: &ServiceRequest) -> Self::Future {
future::ready(
Authorization::<Basic>::parse(req)
.map(|auth| BasicAuth(auth.into_scheme()))
.map_err(|_| {
// TODO: debug! the original error
let challenge = req
.app_data::<Config>()
.map(|config| config.0.clone())
// TODO: Add trace! about `Default::default` call
.unwrap_or_else(Default::default);
AuthenticationError::new(challenge)
}),
)
}
}

View File

@ -0,0 +1,180 @@
//! Extractor for the "Bearer" HTTP Authentication Scheme
use std::borrow::Cow;
use std::default::Default;
use actix_web::dev::{Payload, ServiceRequest};
use actix_web::http::header::Header;
use actix_web::{FromRequest, HttpRequest};
use futures::future;
use super::config::AuthExtractorConfig;
use super::errors::AuthenticationError;
use super::AuthExtractor;
use crate::headers::authorization;
use crate::headers::www_authenticate::bearer;
pub use crate::headers::www_authenticate::bearer::Error;
/// [BearerAuth](./struct/BearerAuth.html) extractor configuration.
#[derive(Debug, Clone, Default)]
pub struct Config(bearer::Bearer);
impl Config {
/// Set challenge `scope` attribute.
///
/// 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.
pub fn scope<T: Into<Cow<'static, str>>>(mut self, value: T) -> Config {
self.0.scope = Some(value.into());
self
}
/// Set challenge `realm` attribute.
///
/// The "realm" attribute indicates the scope of protection in the manner
/// described in HTTP/1.1 [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2).
pub fn realm<T: Into<Cow<'static, str>>>(mut self, value: T) -> Config {
self.0.realm = Some(value.into());
self
}
}
impl AsRef<bearer::Bearer> for Config {
fn as_ref(&self) -> &bearer::Bearer {
&self.0
}
}
impl AuthExtractorConfig for Config {
type Inner = bearer::Bearer;
fn into_inner(self) -> Self::Inner {
self.0
}
}
// Needs `fn main` to display complete example.
#[allow(clippy::needless_doctest_main)]
/// Extractor for HTTP Bearer auth
///
/// # Example
///
/// ```
/// use actix_web_httpauth::extractors::bearer::BearerAuth;
///
/// async fn index(auth: BearerAuth) -> String {
/// 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
///
/// ```
/// use actix_web::{web, App};
/// use actix_web_httpauth::extractors::bearer::{BearerAuth, Config};
///
/// async 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)]
pub struct BearerAuth(authorization::Bearer);
impl BearerAuth {
/// Returns bearer token provided by client.
pub fn token(&self) -> &str {
self.0.token()
}
}
impl FromRequest for BearerAuth {
type Config = Config;
type Future = future::Ready<Result<Self, Self::Error>>;
type Error = AuthenticationError<bearer::Bearer>;
fn from_request(
req: &HttpRequest,
_payload: &mut Payload,
) -> <Self as FromRequest>::Future {
future::ready(
authorization::Authorization::<authorization::Bearer>::parse(req)
.map(|auth| BearerAuth(auth.into_scheme()))
.map_err(|_| {
let bearer = req
.app_data::<Self::Config>()
.map(|config| config.0.clone())
.unwrap_or_else(Default::default);
AuthenticationError::new(bearer)
}),
)
}
}
impl AuthExtractor for BearerAuth {
type Future = future::Ready<Result<Self, Self::Error>>;
type Error = AuthenticationError<bearer::Bearer>;
fn from_service_request(req: &ServiceRequest) -> Self::Future {
future::ready(
authorization::Authorization::<authorization::Bearer>::parse(req)
.map(|auth| BearerAuth(auth.into_scheme()))
.map_err(|_| {
let bearer = req
.app_data::<Config>()
.map(|config| config.0.clone())
.unwrap_or_else(Default::default);
AuthenticationError::new(bearer)
}),
)
}
}
/// Extended error customization for HTTP `Bearer` auth.
impl AuthenticationError<bearer::Bearer> {
/// Attach `Error` to the current Authentication error.
///
/// Error status code will be changed to the one provided by the `kind`
/// Error.
pub fn with_error(mut self, kind: Error) -> Self {
*self.status_code_mut() = kind.status_code();
self.challenge_mut().error = Some(kind);
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
}
/// 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
}
}

View File

@ -0,0 +1,23 @@
use super::AuthenticationError;
use crate::headers::www_authenticate::Challenge;
/// Trait implemented for types that provides configuration
/// for the authentication [extractors].
///
/// [extractors]: ./trait.AuthExtractor.html
pub trait AuthExtractorConfig {
/// Associated challenge type.
type Inner: Challenge;
/// Convert the config instance into a HTTP challenge.
fn into_inner(self) -> Self::Inner;
}
impl<T> From<T> for AuthenticationError<<T as AuthExtractorConfig>::Inner>
where
T: AuthExtractorConfig,
{
fn from(config: T) -> Self {
AuthenticationError::new(config.into_inner())
}
}

View File

@ -0,0 +1,60 @@
use std::error::Error;
use std::fmt;
use actix_web::http::StatusCode;
use actix_web::{HttpResponse, ResponseError};
use crate::headers::www_authenticate::Challenge;
use crate::headers::www_authenticate::WwwAuthenticate;
/// Authentication error returned by authentication extractors.
///
/// Different extractors may extend `AuthenticationError` implementation
/// in order to provide access to inner challenge fields.
#[derive(Debug)]
pub struct AuthenticationError<C: Challenge> {
challenge: C,
status_code: StatusCode,
}
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> {
AuthenticationError {
challenge,
status_code: StatusCode::UNAUTHORIZED,
}
}
/// Returns mutable reference to the inner challenge instance.
pub fn challenge_mut(&mut self) -> &mut C {
&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 {
&mut self.status_code
}
}
impl<C: Challenge> fmt::Display for AuthenticationError<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.status_code, f)
}
}
impl<C: 'static + Challenge> Error for AuthenticationError<C> {}
impl<C: 'static + Challenge> ResponseError for AuthenticationError<C> {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code)
// TODO: Get rid of the `.clone()`
.set(WwwAuthenticate(self.challenge.clone()))
.finish()
}
}

View File

@ -0,0 +1,33 @@
//! Type-safe authentication information extractors
use actix_web::dev::ServiceRequest;
use actix_web::Error;
use futures::future::Future;
pub mod basic;
pub mod bearer;
mod config;
mod errors;
pub use self::config::AuthExtractorConfig;
pub use self::errors::AuthenticationError;
/// Trait implemented by types that can extract
/// HTTP authentication scheme credentials from the request.
///
/// It is very similar to actix' `FromRequest` trait,
/// except it operates with a `ServiceRequest` struct instead,
/// therefore it can be used in the middlewares.
///
/// You will not need it unless you want to implement your own
/// authentication scheme.
pub trait AuthExtractor: Sized {
/// The associated error which can be returned.
type Error: Into<Error>;
/// Future that resolves into extracted credentials type.
type Future: Future<Output = Result<Self, Self::Error>>;
/// Parse the authentication credentials from the actix' `ServiceRequest`.
fn from_service_request(req: &ServiceRequest) -> Self::Future;
}

View 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)
}
}

View 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)
}
}

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

View 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==")
);
}
}

View 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")
);
}
}

View 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>;
}

View File

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

View File

@ -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\"");
}
}

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,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())
}
}

View File

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

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,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)
);
}

View File

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

View 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()
}
}

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

View File

@ -0,0 +1,29 @@
//! HTTP Authorization support for [actix-web](https://actix.rs) framework.
//!
//! Provides:
//! * typed [Authorization] and [WWW-Authenticate] headers
//! * [extractors] for an [Authorization] header
//! * [middleware] for easier authorization checking
//!
//! ## 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/
//! [middleware]: ./middleware/
#![deny(bare_trait_objects)]
#![deny(missing_docs)]
#![deny(nonstandard_style)]
#![deny(rust_2018_idioms)]
#![deny(unused)]
#![deny(clippy::all)]
#![cfg_attr(feature = "nightly", feature(test))]
pub mod extractors;
pub mod headers;
pub mod middleware;
mod utils;

View File

@ -0,0 +1,247 @@
//! HTTP Authentication middleware.
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
use actix_service::{Service, Transform};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::Error;
use futures::future::{self, Future, FutureExt, LocalBoxFuture, TryFutureExt};
use futures::lock::Mutex;
use futures::task::{Context, Poll};
use crate::extractors::{basic, bearer, AuthExtractor};
/// Middleware for checking HTTP authentication.
///
/// If there is no `Authorization` header in the request,
/// this middleware returns an error immediately,
/// without calling the `F` callback.
///
/// Otherwise, it will pass both the request and
/// the parsed credentials into it.
/// In case of successful validation `F` callback
/// is required to return the `ServiceRequest` back.
#[derive(Debug, Clone)]
pub struct HttpAuthentication<T, F>
where
T: AuthExtractor,
{
process_fn: Arc<F>,
_extractor: PhantomData<T>,
}
impl<T, F, O> HttpAuthentication<T, F>
where
T: AuthExtractor,
F: Fn(ServiceRequest, T) -> O,
O: Future<Output = Result<ServiceRequest, Error>>,
{
/// Construct `HttpAuthentication` middleware
/// with the provided auth extractor `T` and
/// validation callback `F`.
pub fn with_fn(process_fn: F) -> HttpAuthentication<T, F> {
HttpAuthentication {
process_fn: Arc::new(process_fn),
_extractor: PhantomData,
}
}
}
impl<F, O> HttpAuthentication<basic::BasicAuth, F>
where
F: Fn(ServiceRequest, basic::BasicAuth) -> O,
O: Future<Output = Result<ServiceRequest, Error>>,
{
/// Construct `HttpAuthentication` middleware for the HTTP "Basic"
/// authentication scheme.
///
/// ## Example
///
/// ```
/// # use actix_web::Error;
/// # use actix_web::dev::ServiceRequest;
/// # use actix_web_httpauth::middleware::HttpAuthentication;
/// # use actix_web_httpauth::extractors::basic::BasicAuth;
/// // In this example validator returns immediately,
/// // but since it is required to return anything
/// // that implements `IntoFuture` trait,
/// // it can be extended to query database
/// // or to do something else in a async manner.
/// async fn validator(
/// req: ServiceRequest,
/// credentials: BasicAuth,
/// ) -> Result<ServiceRequest, Error> {
/// // All users are great and more than welcome!
/// Ok(req)
/// }
///
/// let middleware = HttpAuthentication::basic(validator);
/// ```
pub fn basic(process_fn: F) -> Self {
Self::with_fn(process_fn)
}
}
impl<F, O> HttpAuthentication<bearer::BearerAuth, F>
where
F: Fn(ServiceRequest, bearer::BearerAuth) -> O,
O: Future<Output = Result<ServiceRequest, Error>>,
{
/// Construct `HttpAuthentication` middleware for the HTTP "Bearer"
/// authentication scheme.
///
/// ## Example
///
/// ```
/// # use actix_web::Error;
/// # use actix_web::dev::ServiceRequest;
/// # use actix_web_httpauth::middleware::HttpAuthentication;
/// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth};
/// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig};
/// async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, Error> {
/// if credentials.token() == "mF_9.B5f-4.1JqM" {
/// Ok(req)
/// } else {
/// let config = req.app_data::<Config>()
/// .map(|data| data.get_ref().clone())
/// .unwrap_or_else(Default::default)
/// .scope("urn:example:channel=HBO&urn:example:rating=G,PG-13");
///
/// Err(AuthenticationError::from(config).into())
/// }
/// }
///
/// let middleware = HttpAuthentication::bearer(validator);
/// ```
pub fn bearer(process_fn: F) -> Self {
Self::with_fn(process_fn)
}
}
impl<S, B, T, F, O> Transform<S> for HttpAuthentication<T, F>
where
S: Service<
Request = ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
> + 'static,
S::Future: 'static,
F: Fn(ServiceRequest, T) -> O + 'static,
O: Future<Output = Result<ServiceRequest, Error>> + 'static,
T: AuthExtractor + 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = AuthenticationMiddleware<S, F, T>;
type InitError = ();
type Future = future::Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
future::ok(AuthenticationMiddleware {
service: Arc::new(Mutex::new(service)),
process_fn: self.process_fn.clone(),
_extractor: PhantomData,
})
}
}
#[doc(hidden)]
pub struct AuthenticationMiddleware<S, F, T>
where
T: AuthExtractor,
{
service: Arc<Mutex<S>>,
process_fn: Arc<F>,
_extractor: PhantomData<T>,
}
impl<S, B, F, T, O> Service for AuthenticationMiddleware<S, F, T>
where
S: Service<
Request = ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
> + 'static,
S::Future: 'static,
F: Fn(ServiceRequest, T) -> O + 'static,
O: Future<Output = Result<ServiceRequest, Error>> + 'static,
T: AuthExtractor + 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = S::Error;
type Future = LocalBoxFuture<'static, Result<ServiceResponse<B>, Error>>;
fn poll_ready(
&mut self,
ctx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
self.service
.try_lock()
.expect("AuthenticationMiddleware was called already")
.poll_ready(ctx)
}
fn call(&mut self, req: Self::Request) -> Self::Future {
let process_fn = self.process_fn.clone();
// Note: cloning the mutex, not the service itself
let inner = self.service.clone();
async move {
let (req, credentials) = Extract::<T>::new(req).await?;
let req = process_fn(req, credentials).await?;
let mut service = inner.lock().await;
service.call(req).await
}
.boxed_local()
}
}
struct Extract<T> {
req: Option<ServiceRequest>,
f: Option<LocalBoxFuture<'static, Result<T, Error>>>,
_extractor: PhantomData<fn() -> T>,
}
impl<T> Extract<T> {
pub fn new(req: ServiceRequest) -> Self {
Extract {
req: Some(req),
f: None,
_extractor: PhantomData,
}
}
}
impl<T> Future for Extract<T>
where
T: AuthExtractor,
T::Future: 'static,
T::Error: 'static,
{
type Output = Result<(ServiceRequest, T), Error>;
fn poll(
mut self: Pin<&mut Self>,
ctx: &mut Context<'_>,
) -> Poll<Self::Output> {
if self.f.is_none() {
let req =
self.req.as_ref().expect("Extract future was polled twice!");
let f = T::from_service_request(req).map_err(Into::into);
self.f = Some(f.boxed_local());
}
let f = self
.f
.as_mut()
.expect("Extraction future should be initialized at this point");
let credentials = futures::ready!(Future::poll(f.as_mut(), ctx))?;
let req = self.req.take().expect("Extract future was polled twice!");
Poll::Ready(Ok((req, credentials)))
}
}

View File

@ -0,0 +1,111 @@
use std::str;
use bytes::BytesMut;
enum State {
YieldStr,
YieldQuote,
}
struct Quoted<'a> {
inner: ::std::iter::Peekable<str::Split<'a, char>>,
state: State,
}
impl<'a> Quoted<'a> {
pub fn new(s: &'a str) -> Quoted<'_> {
Quoted {
inner: s.split('"').peekable(),
state: State::YieldStr,
}
}
}
impl<'a> Iterator for Quoted<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
match self.state {
State::YieldStr => match self.inner.next() {
Some(s) => {
self.state = State::YieldQuote;
Some(s)
}
None => None,
},
State::YieldQuote => match self.inner.peek() {
Some(_) => {
self.state = State::YieldStr;
Some("\\\"")
}
None => None,
},
}
}
}
/// Tries to quote the quotes in the passed `value`
pub fn put_quoted(buf: &mut BytesMut, value: &str) {
for part in Quoted::new(value) {
buf.extend_from_slice(part.as_bytes());
}
}
#[cfg(test)]
mod tests {
use std::str;
use bytes::BytesMut;
use super::put_quoted;
#[test]
fn test_quote_str() {
let input = "a \"quoted\" string";
let mut output = BytesMut::new();
put_quoted(&mut output, input);
let result = str::from_utf8(&output).unwrap();
assert_eq!(result, "a \\\"quoted\\\" string");
}
#[test]
fn test_without_quotes() {
let input = "non-quoted string";
let mut output = BytesMut::new();
put_quoted(&mut output, input);
let result = str::from_utf8(&output).unwrap();
assert_eq!(result, "non-quoted string");
}
#[test]
fn test_starts_with_quote() {
let input = "\"first-quoted string";
let mut output = BytesMut::new();
put_quoted(&mut output, input);
let result = str::from_utf8(&output).unwrap();
assert_eq!(result, "\\\"first-quoted string");
}
#[test]
fn test_ends_with_quote() {
let input = "last-quoted string\"";
let mut output = BytesMut::new();
put_quoted(&mut output, input);
let result = str::from_utf8(&output).unwrap();
assert_eq!(result, "last-quoted string\\\"");
}
#[test]
fn test_double_quote() {
let input = "quote\"\"string";
let mut output = BytesMut::new();
put_quoted(&mut output, input);
let result = str::from_utf8(&output).unwrap();
assert_eq!(result, "quote\\\"\\\"string");
}
}