diff --git a/actix-web-httpauth/.editorconfig b/actix-web-httpauth/.editorconfig new file mode 100644 index 000000000..dc6e34dda --- /dev/null +++ b/actix-web-httpauth/.editorconfig @@ -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 diff --git a/actix-web-httpauth/.github/workflows/clippy-and-fmt.yml b/actix-web-httpauth/.github/workflows/clippy-and-fmt.yml new file mode 100644 index 000000000..3ff6c230c --- /dev/null +++ b/actix-web-httpauth/.github/workflows/clippy-and-fmt.yml @@ -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 diff --git a/actix-web-httpauth/.github/workflows/main.yml b/actix-web-httpauth/.github/workflows/main.yml new file mode 100644 index 000000000..071b73855 --- /dev/null +++ b/actix-web-httpauth/.github/workflows/main.yml @@ -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 diff --git a/actix-web-httpauth/.github/workflows/msrv.yml b/actix-web-httpauth/.github/workflows/msrv.yml new file mode 100644 index 000000000..68f1fcecc --- /dev/null +++ b/actix-web-httpauth/.github/workflows/msrv.yml @@ -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 diff --git a/actix-web-httpauth/.gitignore b/actix-web-httpauth/.gitignore new file mode 100644 index 000000000..693699042 --- /dev/null +++ b/actix-web-httpauth/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/actix-web-httpauth/CHANGELOG.md b/actix-web-httpauth/CHANGELOG.md new file mode 100644 index 000000000..8fe17f1f5 --- /dev/null +++ b/actix-web-httpauth/CHANGELOG.md @@ -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 diff --git a/actix-web-httpauth/Cargo.toml b/actix-web-httpauth/Cargo.toml new file mode 100644 index 000000000..8576d509d --- /dev/null +++ b/actix-web-httpauth/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "actix-web-httpauth" +version = "0.4.0" +authors = ["svartalf ", "Yuki Okushi "] +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" } diff --git a/actix-web-httpauth/LICENSE-APACHE b/actix-web-httpauth/LICENSE-APACHE new file mode 100644 index 000000000..7692e1a23 --- /dev/null +++ b/actix-web-httpauth/LICENSE-APACHE @@ -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. diff --git a/actix-web-httpauth/LICENSE-MIT b/actix-web-httpauth/LICENSE-MIT new file mode 100644 index 000000000..44a057e4a --- /dev/null +++ b/actix-web-httpauth/LICENSE-MIT @@ -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. diff --git a/actix-web-httpauth/README.md b/actix-web-httpauth/README.md new file mode 100644 index 000000000..50e9ffab4 --- /dev/null +++ b/actix-web-httpauth/README.md @@ -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) diff --git a/actix-web-httpauth/examples/middleware-closure.rs b/actix-web-httpauth/examples/middleware-closure.rs new file mode 100644 index 000000000..0f623bfe0 --- /dev/null +++ b/actix-web-httpauth/examples/middleware-closure.rs @@ -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 +} diff --git a/actix-web-httpauth/examples/middleware.rs b/actix-web-httpauth/examples/middleware.rs new file mode 100644 index 000000000..672e08d90 --- /dev/null +++ b/actix-web-httpauth/examples/middleware.rs @@ -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 { + 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 +} diff --git a/actix-web-httpauth/rustfmt.toml b/actix-web-httpauth/rustfmt.toml new file mode 100644 index 000000000..f944c9712 --- /dev/null +++ b/actix-web-httpauth/rustfmt.toml @@ -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 diff --git a/actix-web-httpauth/src/extractors/basic.rs b/actix-web-httpauth/src/extractors/basic.rs new file mode 100644 index 000000000..467be7f02 --- /dev/null +++ b/actix-web-httpauth/src/extractors/basic.rs @@ -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(mut self, value: T) -> Config + where + T: Into>, + { + self.0.realm = Some(value.into()); + self + } +} + +impl AsRef 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>; + type Config = Config; + type Error = AuthenticationError; + + fn from_request( + req: &HttpRequest, + _: &mut Payload, + ) -> ::Future { + future::ready( + Authorization::::parse(req) + .map(|auth| BasicAuth(auth.into_scheme())) + .map_err(|_| { + // TODO: debug! the original error + let challenge = req + .app_data::() + .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; + type Future = future::Ready>; + + fn from_service_request(req: &ServiceRequest) -> Self::Future { + future::ready( + Authorization::::parse(req) + .map(|auth| BasicAuth(auth.into_scheme())) + .map_err(|_| { + // TODO: debug! the original error + let challenge = req + .app_data::() + .map(|config| config.0.clone()) + // TODO: Add trace! about `Default::default` call + .unwrap_or_else(Default::default); + + AuthenticationError::new(challenge) + }), + ) + } +} diff --git a/actix-web-httpauth/src/extractors/bearer.rs b/actix-web-httpauth/src/extractors/bearer.rs new file mode 100644 index 000000000..566e77b8d --- /dev/null +++ b/actix-web-httpauth/src/extractors/bearer.rs @@ -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>>(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>>(mut self, value: T) -> Config { + self.0.realm = Some(value.into()); + self + } +} + +impl AsRef 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>; + type Error = AuthenticationError; + + fn from_request( + req: &HttpRequest, + _payload: &mut Payload, + ) -> ::Future { + future::ready( + authorization::Authorization::::parse(req) + .map(|auth| BearerAuth(auth.into_scheme())) + .map_err(|_| { + let bearer = req + .app_data::() + .map(|config| config.0.clone()) + .unwrap_or_else(Default::default); + + AuthenticationError::new(bearer) + }), + ) + } +} + +impl AuthExtractor for BearerAuth { + type Future = future::Ready>; + type Error = AuthenticationError; + + fn from_service_request(req: &ServiceRequest) -> Self::Future { + future::ready( + authorization::Authorization::::parse(req) + .map(|auth| BearerAuth(auth.into_scheme())) + .map_err(|_| { + let bearer = req + .app_data::() + .map(|config| config.0.clone()) + .unwrap_or_else(Default::default); + + AuthenticationError::new(bearer) + }), + ) + } +} + +/// Extended error customization for HTTP `Bearer` auth. +impl AuthenticationError { + /// 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(mut self, desc: T) -> Self + where + T: Into>, + { + 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(mut self, uri: T) -> Self + where + T: Into>, + { + self.challenge_mut().error_uri = Some(uri.into()); + self + } +} diff --git a/actix-web-httpauth/src/extractors/config.rs b/actix-web-httpauth/src/extractors/config.rs new file mode 100644 index 000000000..2faf869d9 --- /dev/null +++ b/actix-web-httpauth/src/extractors/config.rs @@ -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 From for AuthenticationError<::Inner> +where + T: AuthExtractorConfig, +{ + fn from(config: T) -> Self { + AuthenticationError::new(config.into_inner()) + } +} diff --git a/actix-web-httpauth/src/extractors/errors.rs b/actix-web-httpauth/src/extractors/errors.rs new file mode 100644 index 000000000..c136d6617 --- /dev/null +++ b/actix-web-httpauth/src/extractors/errors.rs @@ -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 { + challenge: C, + status_code: StatusCode, +} + +impl AuthenticationError { + /// 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 { + 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 fmt::Display for AuthenticationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.status_code, f) + } +} + +impl Error for AuthenticationError {} + +impl ResponseError for AuthenticationError { + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code) + // TODO: Get rid of the `.clone()` + .set(WwwAuthenticate(self.challenge.clone())) + .finish() + } +} diff --git a/actix-web-httpauth/src/extractors/mod.rs b/actix-web-httpauth/src/extractors/mod.rs new file mode 100644 index 000000000..27a3e4e33 --- /dev/null +++ b/actix-web-httpauth/src/extractors/mod.rs @@ -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; + + /// Future that resolves into extracted credentials type. + type Future: Future>; + + /// Parse the authentication credentials from the actix' `ServiceRequest`. + fn from_service_request(req: &ServiceRequest) -> Self::Future; +} diff --git a/actix-web-httpauth/src/headers/authorization/errors.rs b/actix-web-httpauth/src/headers/authorization/errors.rs new file mode 100644 index 000000000..f2c620063 --- /dev/null +++ b/actix-web-httpauth/src/headers/authorization/errors.rs @@ -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 for ParseError { + fn from(e: header::ToStrError) -> Self { + ParseError::ToStrError(e) + } +} +impl From for ParseError { + fn from(e: base64::DecodeError) -> Self { + ParseError::Base64DecodeError(e) + } +} +impl From for ParseError { + fn from(e: str::Utf8Error) -> Self { + ParseError::Utf8Error(e) + } +} diff --git a/actix-web-httpauth/src/headers/authorization/header.rs b/actix-web-httpauth/src/headers/authorization/header.rs new file mode 100644 index 000000000..3fb9531d2 --- /dev/null +++ b/actix-web-httpauth/src/headers/authorization/header.rs @@ -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 { +/// let auth = Authorization::::parse(&req)?; +/// +/// Ok(format!("Hello, {}!", auth.as_ref().user_id())) +/// } +/// ``` +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)] +pub struct Authorization(S); + +impl Authorization +where + S: Scheme, +{ + /// Consumes `Authorization` header and returns inner [`Scheme`] + /// implementation. + /// + /// [`Scheme`]: ./trait.Scheme.html + pub fn into_scheme(self) -> S { + self.0 + } +} + +impl From for Authorization +where + S: Scheme, +{ + fn from(scheme: S) -> Authorization { + Authorization(scheme) + } +} + +impl AsRef for Authorization +where + S: Scheme, +{ + fn as_ref(&self) -> &S { + &self.0 + } +} + +impl AsMut for Authorization +where + S: Scheme, +{ + fn as_mut(&mut self) -> &mut S { + &mut self.0 + } +} + +impl Header for Authorization { + #[inline] + fn name() -> HeaderName { + AUTHORIZATION + } + + fn parse(msg: &T) -> Result { + let header = + msg.headers().get(AUTHORIZATION).ok_or(ParseError::Header)?; + let scheme = S::parse(header).map_err(|_| ParseError::Header)?; + + Ok(Authorization(scheme)) + } +} + +impl IntoHeaderValue for Authorization { + type Error = ::Error; + + fn try_into(self) -> Result::Error> { + self.0.try_into() + } +} + +impl fmt::Display for Authorization { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} diff --git a/actix-web-httpauth/src/headers/authorization/mod.rs b/actix-web-httpauth/src/headers/authorization/mod.rs new file mode 100644 index 000000000..b02e0531d --- /dev/null +++ b/actix-web-httpauth/src/headers/authorization/mod.rs @@ -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; diff --git a/actix-web-httpauth/src/headers/authorization/scheme/basic.rs b/actix-web-httpauth/src/headers/authorization/scheme/basic.rs new file mode 100644 index 000000000..107217ba3 --- /dev/null +++ b/actix-web-httpauth/src/headers/authorization/scheme/basic.rs @@ -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>, +} + +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(user_id: U, password: Option

) -> Basic + where + U: Into>, + P: Into>, + { + 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 { + // "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::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==") + ); + } +} diff --git a/actix-web-httpauth/src/headers/authorization/scheme/bearer.rs b/actix-web-httpauth/src/headers/authorization/scheme/bearer.rs new file mode 100644 index 000000000..ed52ee1b5 --- /dev/null +++ b/actix-web-httpauth/src/headers/authorization/scheme/bearer.rs @@ -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(token: T) -> Bearer + where + T: Into>, + { + 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 { + // "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::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") + ); + } +} diff --git a/actix-web-httpauth/src/headers/authorization/scheme/mod.rs b/actix-web-httpauth/src/headers/authorization/scheme/mod.rs new file mode 100644 index 000000000..1ab534c93 --- /dev/null +++ b/actix-web-httpauth/src/headers/authorization/scheme/mod.rs @@ -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; +} diff --git a/actix-web-httpauth/src/headers/mod.rs b/actix-web-httpauth/src/headers/mod.rs new file mode 100644 index 000000000..5e38e661a --- /dev/null +++ b/actix-web-httpauth/src/headers/mod.rs @@ -0,0 +1,4 @@ +//! Typed HTTP headers + +pub mod authorization; +pub mod www_authenticate; diff --git a/actix-web-httpauth/src/headers/www_authenticate/challenge/basic.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/basic.rs new file mode 100644 index 000000000..1a01c965d --- /dev/null +++ b/actix-web-httpauth/src/headers/www_authenticate/challenge/basic.rs @@ -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>, +} + +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(value: T) -> Basic + where + T: Into>, + { + 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::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\""); + } +} diff --git a/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/builder.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/builder.rs new file mode 100644 index 000000000..b4bf11455 --- /dev/null +++ b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/builder.rs @@ -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(mut self, value: T) -> Self + where + T: Into>, + { + self.0.scope = Some(value.into()); + self + } + + /// Provides the `realm` attribute, as defined in [RFC2617](https://tools.ietf.org/html/rfc2617) + pub fn realm(mut self, value: T) -> Self + where + T: Into>, + { + 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(mut self, value: T) -> Self + where + T: Into>, + { + 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(mut self, value: T) -> Self + where + T: Into>, + { + self.0.error_uri = Some(value.into()); + self + } + + /// Consumes the builder and returns built `Bearer` instance. + pub fn finish(self) -> Bearer { + self.0 + } +} diff --git a/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/challenge.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/challenge.rs new file mode 100644 index 000000000..9207b59d0 --- /dev/null +++ b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/challenge.rs @@ -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>, + pub(crate) realm: Option>, + pub(crate) error: Option, + pub(crate) error_description: Option>, + pub(crate) error_uri: Option>, +} + +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::Error> { + HeaderValue::from_maybe_shared(self.to_bytes()) + } +} diff --git a/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/errors.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/errors.rs new file mode 100644 index 000000000..fb2f9a3dd --- /dev/null +++ b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/errors.rs @@ -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()) + } +} diff --git a/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/mod.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/mod.rs new file mode 100644 index 000000000..d51237c45 --- /dev/null +++ b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/mod.rs @@ -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; diff --git a/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/tests.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/tests.rs new file mode 100644 index 000000000..015c07d33 --- /dev/null +++ b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/tests.rs @@ -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) + ); +} diff --git a/actix-web-httpauth/src/headers/www_authenticate/challenge/mod.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/mod.rs new file mode 100644 index 000000000..619c5ef51 --- /dev/null +++ b/actix-web-httpauth/src/headers/www_authenticate/challenge/mod.rs @@ -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; +} diff --git a/actix-web-httpauth/src/headers/www_authenticate/header.rs b/actix-web-httpauth/src/headers/www_authenticate/header.rs new file mode 100644 index 000000000..092b7593b --- /dev/null +++ b/actix-web-httpauth/src/headers/www_authenticate/header.rs @@ -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(pub C); + +impl Header for WwwAuthenticate { + fn name() -> HeaderName { + WWW_AUTHENTICATE + } + + fn parse(_msg: &T) -> Result { + unimplemented!() + } +} + +impl IntoHeaderValue for WwwAuthenticate { + type Error = ::Error; + + fn try_into(self) -> Result::Error> { + self.0.try_into() + } +} diff --git a/actix-web-httpauth/src/headers/www_authenticate/mod.rs b/actix-web-httpauth/src/headers/www_authenticate/mod.rs new file mode 100644 index 000000000..6262f050a --- /dev/null +++ b/actix-web-httpauth/src/headers/www_authenticate/mod.rs @@ -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; diff --git a/actix-web-httpauth/src/lib.rs b/actix-web-httpauth/src/lib.rs new file mode 100644 index 000000000..2807fa07d --- /dev/null +++ b/actix-web-httpauth/src/lib.rs @@ -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; diff --git a/actix-web-httpauth/src/middleware.rs b/actix-web-httpauth/src/middleware.rs new file mode 100644 index 000000000..f0ce7fc28 --- /dev/null +++ b/actix-web-httpauth/src/middleware.rs @@ -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 +where + T: AuthExtractor, +{ + process_fn: Arc, + _extractor: PhantomData, +} + +impl HttpAuthentication +where + T: AuthExtractor, + F: Fn(ServiceRequest, T) -> O, + O: Future>, +{ + /// Construct `HttpAuthentication` middleware + /// with the provided auth extractor `T` and + /// validation callback `F`. + pub fn with_fn(process_fn: F) -> HttpAuthentication { + HttpAuthentication { + process_fn: Arc::new(process_fn), + _extractor: PhantomData, + } + } +} + +impl HttpAuthentication +where + F: Fn(ServiceRequest, basic::BasicAuth) -> O, + O: Future>, +{ + /// 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 { + /// // 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 HttpAuthentication +where + F: Fn(ServiceRequest, bearer::BearerAuth) -> O, + O: Future>, +{ + /// 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 { + /// if credentials.token() == "mF_9.B5f-4.1JqM" { + /// Ok(req) + /// } else { + /// let config = req.app_data::() + /// .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 Transform for HttpAuthentication +where + S: Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + S::Future: 'static, + F: Fn(ServiceRequest, T) -> O + 'static, + O: Future> + 'static, + T: AuthExtractor + 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Transform = AuthenticationMiddleware; + type InitError = (); + type Future = future::Ready>; + + 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 +where + T: AuthExtractor, +{ + service: Arc>, + process_fn: Arc, + _extractor: PhantomData, +} + +impl Service for AuthenticationMiddleware +where + S: Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + S::Future: 'static, + F: Fn(ServiceRequest, T) -> O + 'static, + O: Future> + 'static, + T: AuthExtractor + 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = S::Error; + type Future = LocalBoxFuture<'static, Result, Error>>; + + fn poll_ready( + &mut self, + ctx: &mut Context<'_>, + ) -> Poll> { + 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::::new(req).await?; + let req = process_fn(req, credentials).await?; + let mut service = inner.lock().await; + service.call(req).await + } + .boxed_local() + } +} + +struct Extract { + req: Option, + f: Option>>, + _extractor: PhantomData T>, +} + +impl Extract { + pub fn new(req: ServiceRequest) -> Self { + Extract { + req: Some(req), + f: None, + _extractor: PhantomData, + } + } +} + +impl Future for Extract +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 { + 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))) + } +} diff --git a/actix-web-httpauth/src/utils.rs b/actix-web-httpauth/src/utils.rs new file mode 100644 index 000000000..b9296b037 --- /dev/null +++ b/actix-web-httpauth/src/utils.rs @@ -0,0 +1,111 @@ +use std::str; + +use bytes::BytesMut; + +enum State { + YieldStr, + YieldQuote, +} + +struct Quoted<'a> { + inner: ::std::iter::Peekable>, + 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 { + 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"); + } +}