From 8703f2f82e36171515ca488fca0163c42f738a3a Mon Sep 17 00:00:00 2001 From: svartalf Date: Sun, 20 May 2018 12:59:41 +0300 Subject: [PATCH 01/36] Initial commit --- .gitignore | 3 + .travis.yml | 8 ++ Cargo.toml | 16 ++++ LICENSE-APACHE | 201 +++++++++++++++++++++++++++++++++++++++++++ LICENSE-MIT | 25 ++++++ README.md | 1 + src/errors.rs | 63 ++++++++++++++ src/lib.rs | 8 ++ src/schemes/basic.rs | 120 ++++++++++++++++++++++++++ src/schemes/mod.rs | 3 + 10 files changed, 448 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 src/errors.rs create mode 100644 src/lib.rs create mode 100644 src/schemes/basic.rs create mode 100644 src/schemes/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..693699042 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..8c91a7415 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: rust +rust: + - stable + - beta + - nightly +matrix: + allow_failures: + - rust: nightly diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..773d4c264 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "actix-web-httpauth" +version = "0.0.1" +authors = ["svartalf "] +description = "HTTP authorization schemes for actix-web" +readme = "README.md" +keywords = ["http", "web", "framework"] +homepage = "https://github.com/svartalf/actix-web-httpauth" +repository = "https://github.com/svartlaf/actix-web-httpauth.git" +documentation = "https://docs.rs/actix-web-httpauth/" +categories = ["web-programming::http-server"] +license = "MIT/Apache-2.0" + +[dependencies] +actix-web = "0.6" +base64 = "0.9" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 000000000..b3844ab27 --- /dev/null +++ b/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 + + 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/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 000000000..a82012880 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2017 svartalf + +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/README.md b/README.md new file mode 100644 index 000000000..cd9f91f30 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# actix-web-httpauth diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 000000000..f85fd7fd9 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,63 @@ +use std::fmt; +use std::error; +use std::string; + +use base64; +use actix_web::HttpResponse; +use actix_web::error::ResponseError; +use actix_web::http::{StatusCode, header}; + +#[derive(Debug, PartialEq)] +pub enum Error { + HeaderMissing, // HTTP 401 + // TODO: Ensure that 401 should be returned if not a `Basic` mechanism is received + InvalidMechanism, // HTTP 401 ? + HeaderMalformed, // HTTP 400 +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let msg = match self { + Error::HeaderMissing => "HTTP 'Authorization' header is missing", + Error::InvalidMechanism => "Wrong mechanizm for a HTTP 'Authorization' header, expected 'Basic'", + Error::HeaderMalformed => "Malformed HTTP 'Authorization' header", + }; + + f.write_str(msg) + } +} + +impl error::Error for Error {} + +impl From for Error { + fn from(_: header::ToStrError) -> Self { + Error::HeaderMalformed + } +} + +impl From for Error { + fn from(_: base64::DecodeError) -> Self { + Error::HeaderMalformed + } +} + +impl From for Error { + fn from(_: string::FromUtf8Error) -> Self { + Error::HeaderMalformed + } +} + +impl ResponseError for Error { + fn error_response(&self) -> HttpResponse { + let status = match self { + Error::HeaderMissing => StatusCode::UNAUTHORIZED, + Error::InvalidMechanism => StatusCode::UNAUTHORIZED, + Error::HeaderMalformed => StatusCode::BAD_REQUEST, + }; + + HttpResponse::build(status) + .header("WWW-Authenticate", "Basic, charset=\"UTF-8\"") + .finish() + } + +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..67bbfb6f1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +extern crate actix_web; +extern crate base64; + +mod schemes; +mod errors; + +pub use schemes::*; +pub use errors::Error; diff --git a/src/schemes/basic.rs b/src/schemes/basic.rs new file mode 100644 index 000000000..3000abaec --- /dev/null +++ b/src/schemes/basic.rs @@ -0,0 +1,120 @@ +use base64; +use actix_web::{HttpRequest, HttpMessage, FromRequest}; + +use errors::Error; + +/// Extractor for `Authorization: Basic {payload}` HTTP request header. +/// +/// If header is not present, HTTP 401 will be returned. +/// +/// # Example +/// +/// use actix_web_httpauth::BasicAuth; +/// +/// pub fn handler(auth: BasicAuth) -> String { +/// format!("Hello, {}", auth.username) +/// } +#[derive(Debug, PartialEq)] +pub struct BasicAuth { + pub username: String, + pub password: String, +} + + +impl FromRequest for BasicAuth { + type Config = (); + type Result = Result; + + fn from_request(req: &HttpRequest, _cfg: &>::Config) -> >::Result { + let header = req.headers().get("Authorization") + .ok_or(Error::HeaderMissing)? + .to_str()?; + let mut parts = header.splitn(2, ' '); + + // Authorization mechanism + match parts.next() { + None => return Err(Error::InvalidMechanism), + Some(mechanism) if mechanism != "Basic" => return Err(Error::InvalidMechanism), + _ => () + } + + // Authorization payload + let payload = parts.next().ok_or(Error::HeaderMalformed)?; + let payload = base64::decode(payload)?; + let payload = String::from_utf8(payload)?; + let mut parts = payload.splitn(2, ':'); + let user = parts.next().ok_or(Error::HeaderMalformed)?; + let password = parts.next().ok_or(Error::HeaderMalformed)?; + + Ok(BasicAuth{ + username: user.to_string(), + password: password.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use base64; + use actix_web::FromRequest; + use actix_web::test::TestRequest; + + use super::{BasicAuth, Error}; + + #[test] + fn test_valid_auth() { + let value = format!("Basic {}", base64::encode("user:pass")); + let req = TestRequest::with_header("Authorization", value).finish(); + let auth = BasicAuth::extract(&req); + + assert!(auth.is_ok()); + let auth = auth.unwrap(); + assert_eq!(auth.username, "user".to_string()); + assert_eq!(auth.password, "pass".to_string()); + } + + #[test] + fn test_missing_header() { + let req = TestRequest::default().finish(); + let auth = BasicAuth::extract(&req); + + assert!(auth.is_err()); + let err = auth.err().unwrap(); + assert_eq!(err, Error::HeaderMissing); + } + + #[test] + fn test_invalid_mechanism() { + let value = format!("Digest {}", base64::encode("user:pass")); + let req = TestRequest::with_header("Authorization", value).finish(); + let auth = BasicAuth::extract(&req); + + assert!(auth.is_err()); + let err = auth.err().unwrap(); + assert_eq!(err, Error::InvalidMechanism); + } + + #[test] + fn test_invalid_format() { + let value = format!("Basic {}", base64::encode("user")); + let req = TestRequest::with_header("Authorization", value).finish(); + let auth = BasicAuth::extract(&req); + + assert!(auth.is_err()); + let err = auth.err().unwrap(); + assert_eq!(err, Error::HeaderMalformed); + } + + #[test] + fn test_user_without_password() { + let value = format!("Basic {}", base64::encode("user:")); + let req = TestRequest::with_header("Authorization", value).finish(); + let auth = BasicAuth::extract(&req); + + assert!(auth.is_ok()); + assert_eq!(auth.unwrap(), BasicAuth { + username: "user".to_string(), + password: "".to_string(), + }) + } +} diff --git a/src/schemes/mod.rs b/src/schemes/mod.rs new file mode 100644 index 000000000..43d195913 --- /dev/null +++ b/src/schemes/mod.rs @@ -0,0 +1,3 @@ +mod basic; + +pub use self::basic::BasicAuth; From 5964f4d767b8ea45cebefa2614ea750fdd1a9f8b Mon Sep 17 00:00:00 2001 From: svartalf Date: Sun, 20 May 2018 13:15:59 +0300 Subject: [PATCH 02/36] Badges and stable fixes --- Cargo.toml | 4 ++++ README.md | 6 ++++++ src/errors.rs | 46 +++++++++++++++++++++++--------------------- src/lib.rs | 2 +- src/schemes/basic.rs | 24 +++++++++++------------ 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 773d4c264..ca6bde05c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,10 @@ repository = "https://github.com/svartlaf/actix-web-httpauth.git" documentation = "https://docs.rs/actix-web-httpauth/" categories = ["web-programming::http-server"] license = "MIT/Apache-2.0" +exclude = [".travis.yml", ".gitignore"] + +[badges] +travis-ci = { repository = "svartalf/actix-web-httpauth", branch = "master" } [dependencies] actix-web = "0.6" diff --git a/README.md b/README.md index cd9f91f30..89d3a8f63 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ # actix-web-httpauth + +[![Build Status](https://travis-ci.org/svartalf/actix-web-httpauth.svg?branch=master)] +[![Docs](https://docs.rs/actix-web-httpauth/badge.svg)] +[![Crates.io](https://img.shields.io/crates/v/actix-web-httpauth.svg)] + +HTTP authorization schemes for [actix-web](https://github.com/actix/actix-web) framework. diff --git a/src/errors.rs b/src/errors.rs index f85fd7fd9..8870d0a19 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,5 @@ use std::fmt; -use std::error; +use std::error::Error; use std::string; use base64; @@ -8,51 +8,53 @@ use actix_web::error::ResponseError; use actix_web::http::{StatusCode, header}; #[derive(Debug, PartialEq)] -pub enum Error { +pub enum AuthError { HeaderMissing, // HTTP 401 // TODO: Ensure that 401 should be returned if not a `Basic` mechanism is received InvalidMechanism, // HTTP 401 ? HeaderMalformed, // HTTP 400 } -impl fmt::Display for Error { +impl fmt::Display for AuthError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let msg = match self { - Error::HeaderMissing => "HTTP 'Authorization' header is missing", - Error::InvalidMechanism => "Wrong mechanizm for a HTTP 'Authorization' header, expected 'Basic'", - Error::HeaderMalformed => "Malformed HTTP 'Authorization' header", - }; - - f.write_str(msg) + f.write_str(self.description()) } } -impl error::Error for Error {} +impl Error for AuthError { + fn description(&self) -> &str { + match *self { + AuthError::HeaderMissing => "HTTP 'Authorization' header is missing", + AuthError::InvalidMechanism => "Wrong mechanism for a HTTP 'Authorization' header, expected 'Basic'", + AuthError::HeaderMalformed => "Malformed HTTP 'Authorization' header", + } + } +} -impl From for Error { +impl From for AuthError { fn from(_: header::ToStrError) -> Self { - Error::HeaderMalformed + AuthError::HeaderMalformed } } -impl From for Error { +impl From for AuthError { fn from(_: base64::DecodeError) -> Self { - Error::HeaderMalformed + AuthError::HeaderMalformed } } -impl From for Error { +impl From for AuthError { fn from(_: string::FromUtf8Error) -> Self { - Error::HeaderMalformed + AuthError::HeaderMalformed } } -impl ResponseError for Error { +impl ResponseError for AuthError { fn error_response(&self) -> HttpResponse { - let status = match self { - Error::HeaderMissing => StatusCode::UNAUTHORIZED, - Error::InvalidMechanism => StatusCode::UNAUTHORIZED, - Error::HeaderMalformed => StatusCode::BAD_REQUEST, + let status = match *self { + AuthError::HeaderMissing => StatusCode::UNAUTHORIZED, + AuthError::InvalidMechanism => StatusCode::UNAUTHORIZED, + AuthError::HeaderMalformed => StatusCode::BAD_REQUEST, }; HttpResponse::build(status) diff --git a/src/lib.rs b/src/lib.rs index 67bbfb6f1..1b1d0654b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,4 +5,4 @@ mod schemes; mod errors; pub use schemes::*; -pub use errors::Error; +pub use errors::AuthError; diff --git a/src/schemes/basic.rs b/src/schemes/basic.rs index 3000abaec..28afbf7df 100644 --- a/src/schemes/basic.rs +++ b/src/schemes/basic.rs @@ -1,7 +1,7 @@ use base64; use actix_web::{HttpRequest, HttpMessage, FromRequest}; -use errors::Error; +use errors::AuthError; /// Extractor for `Authorization: Basic {payload}` HTTP request header. /// @@ -23,28 +23,28 @@ pub struct BasicAuth { impl FromRequest for BasicAuth { type Config = (); - type Result = Result; + type Result = Result; fn from_request(req: &HttpRequest, _cfg: &>::Config) -> >::Result { let header = req.headers().get("Authorization") - .ok_or(Error::HeaderMissing)? + .ok_or(AuthError::HeaderMissing)? .to_str()?; let mut parts = header.splitn(2, ' '); // Authorization mechanism match parts.next() { - None => return Err(Error::InvalidMechanism), - Some(mechanism) if mechanism != "Basic" => return Err(Error::InvalidMechanism), + None => return Err(AuthError::InvalidMechanism), + Some(mechanism) if mechanism != "Basic" => return Err(AuthError::InvalidMechanism), _ => () } // Authorization payload - let payload = parts.next().ok_or(Error::HeaderMalformed)?; + let payload = parts.next().ok_or(AuthError::HeaderMalformed)?; let payload = base64::decode(payload)?; let payload = String::from_utf8(payload)?; let mut parts = payload.splitn(2, ':'); - let user = parts.next().ok_or(Error::HeaderMalformed)?; - let password = parts.next().ok_or(Error::HeaderMalformed)?; + let user = parts.next().ok_or(AuthError::HeaderMalformed)?; + let password = parts.next().ok_or(AuthError::HeaderMalformed)?; Ok(BasicAuth{ username: user.to_string(), @@ -59,7 +59,7 @@ mod tests { use actix_web::FromRequest; use actix_web::test::TestRequest; - use super::{BasicAuth, Error}; + use super::{BasicAuth, AuthError}; #[test] fn test_valid_auth() { @@ -80,7 +80,7 @@ mod tests { assert!(auth.is_err()); let err = auth.err().unwrap(); - assert_eq!(err, Error::HeaderMissing); + assert_eq!(err, AuthError::HeaderMissing); } #[test] @@ -91,7 +91,7 @@ mod tests { assert!(auth.is_err()); let err = auth.err().unwrap(); - assert_eq!(err, Error::InvalidMechanism); + assert_eq!(err, AuthError::InvalidMechanism); } #[test] @@ -102,7 +102,7 @@ mod tests { assert!(auth.is_err()); let err = auth.err().unwrap(); - assert_eq!(err, Error::HeaderMalformed); + assert_eq!(err, AuthError::HeaderMalformed); } #[test] From 65578ead0236be83840ee519a779f572124e2491 Mon Sep 17 00:00:00 2001 From: svartalf Date: Sun, 20 May 2018 13:16:56 +0300 Subject: [PATCH 03/36] One more badges update --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 89d3a8f63..fa9fa396f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # actix-web-httpauth -[![Build Status](https://travis-ci.org/svartalf/actix-web-httpauth.svg?branch=master)] -[![Docs](https://docs.rs/actix-web-httpauth/badge.svg)] -[![Crates.io](https://img.shields.io/crates/v/actix-web-httpauth.svg)] +![Build Status](https://travis-ci.org/svartalf/actix-web-httpauth.svg?branch=master) +![Docs](https://docs.rs/actix-web-httpauth/badge.svg) +![Crates.io](https://img.shields.io/crates/v/actix-web-httpauth.svg) HTTP authorization schemes for [actix-web](https://github.com/actix/actix-web) framework. From d5eb47c6e0b543e3f95a8c713b69d4195dfbade9 Mon Sep 17 00:00:00 2001 From: svartalf Date: Sun, 20 May 2018 22:10:48 +0300 Subject: [PATCH 04/36] Added an example for Basic authentication --- Cargo.toml | 4 ++-- README.md | 9 ++++++++- examples/basic.rs | 41 +++++++++++++++++++++++++++++++++++++++++ src/errors.rs | 2 +- src/schemes/basic.rs | 2 ++ 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 examples/basic.rs diff --git a/Cargo.toml b/Cargo.toml index ca6bde05c..7ac8b4243 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,11 @@ name = "actix-web-httpauth" version = "0.0.1" authors = ["svartalf "] -description = "HTTP authorization schemes for actix-web" +description = "HTTP authentication schemes for actix-web" readme = "README.md" keywords = ["http", "web", "framework"] homepage = "https://github.com/svartalf/actix-web-httpauth" -repository = "https://github.com/svartlaf/actix-web-httpauth.git" +repository = "https://github.com/svartalf/actix-web-httpauth.git" documentation = "https://docs.rs/actix-web-httpauth/" categories = ["web-programming::http-server"] license = "MIT/Apache-2.0" diff --git a/README.md b/README.md index fa9fa396f..32011ba0d 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,11 @@ ![Docs](https://docs.rs/actix-web-httpauth/badge.svg) ![Crates.io](https://img.shields.io/crates/v/actix-web-httpauth.svg) -HTTP authorization schemes for [actix-web](https://github.com/actix/actix-web) framework. +HTTP authentication schemes for [actix-web](https://github.com/actix/actix-web) framework. + +All supported schemas are actix [Extractors](https://docs.rs/actix-web/0.6.7/actix_web/trait.FromRequest.html), +and can be used both in middlewares and request handlers, check the `examples/` folder. + +## Supported schemes + + * [Basic](https://tools.ietf.org/html/rfc7617) diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 000000000..2ff715fd9 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,41 @@ +extern crate actix_web; +extern crate actix_web_httpauth; + +use actix_web::http::StatusCode; +use actix_web::{server, App, HttpRequest, FromRequest, Result}; +use actix_web::middleware::{Middleware, Started}; +use actix_web_httpauth::BasicAuth; + +struct AuthMiddleware; + +impl Middleware for AuthMiddleware { + fn start(&self, req: &mut HttpRequest) -> Result { + let auth = BasicAuth::extract(&req)?; + + // Please note that this is only an example, + // do not ever hardcode your credentials! + if auth.username == "root" && auth.password == "pass" { + Ok(Started::Done) + } else { + let response = req.build_response(StatusCode::UNAUTHORIZED) + .header("WWW-Authenticate", "Basic") + .finish(); + Ok(Started::Response(response)) + } + } + +} + +fn index(auth: BasicAuth) -> String { + format!("Hello, {}", auth.username) +} + +fn main() { + server::new(|| App::new() + // Comment line below to pass authentication handling + // directly to `index` handler. + .middleware(AuthMiddleware) + .resource("/", |r| r.with(index))) + .bind("127.0.0.1:8088").unwrap() + .run(); +} diff --git a/src/errors.rs b/src/errors.rs index 8870d0a19..59ccd3db5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -58,7 +58,7 @@ impl ResponseError for AuthError { }; HttpResponse::build(status) - .header("WWW-Authenticate", "Basic, charset=\"UTF-8\"") + .header("WWW-Authenticate", "Basic") .finish() } diff --git a/src/schemes/basic.rs b/src/schemes/basic.rs index 28afbf7df..89990c5bf 100644 --- a/src/schemes/basic.rs +++ b/src/schemes/basic.rs @@ -9,11 +9,13 @@ use errors::AuthError; /// /// # Example /// +/// ```rust /// use actix_web_httpauth::BasicAuth; /// /// pub fn handler(auth: BasicAuth) -> String { /// format!("Hello, {}", auth.username) /// } +/// ``` #[derive(Debug, PartialEq)] pub struct BasicAuth { pub username: String, From 082e8371c1459ceaf4ef4c09077b1bd9c27449a7 Mon Sep 17 00:00:00 2001 From: svartalf Date: Sun, 20 May 2018 22:15:59 +0300 Subject: [PATCH 05/36] Fixing link to HTTP Basic authentication RFC7617 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32011ba0d..7b199822a 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,4 @@ and can be used both in middlewares and request handlers, check the `examples/` ## Supported schemes - * [Basic](https://tools.ietf.org/html/rfc7617) + * [Basic](https://tools.ietf.org/html/rfc7617) From f64c1e88793e2dc5646aed59017efe921ee99e81 Mon Sep 17 00:00:00 2001 From: svartalf Date: Wed, 23 May 2018 15:31:14 +0300 Subject: [PATCH 06/36] Working version of the BasicAuth --- Cargo.toml | 2 + examples/basic.rs | 15 +++---- src/basic/config.rs | 61 ++++++++++++++++++++++++++ src/basic/mod.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++ src/basic/tests.rs | 56 ++++++++++++++++++++++++ src/challenge.rs | 6 +++ src/errors.rs | 74 +++++++++++--------------------- src/lib.rs | 13 ++++-- 8 files changed, 267 insertions(+), 61 deletions(-) create mode 100644 src/basic/config.rs create mode 100644 src/basic/mod.rs create mode 100644 src/basic/tests.rs create mode 100644 src/challenge.rs diff --git a/Cargo.toml b/Cargo.toml index 7ac8b4243..8aac469dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,5 @@ travis-ci = { repository = "svartalf/actix-web-httpauth", branch = "master" } [dependencies] actix-web = "0.6" base64 = "0.9" +percent-encoding = "1.0.1" +bytes = "0.4.7" \ No newline at end of file diff --git a/examples/basic.rs b/examples/basic.rs index 2ff715fd9..3a32eb938 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,25 +1,24 @@ extern crate actix_web; extern crate actix_web_httpauth; -use actix_web::http::StatusCode; use actix_web::{server, App, HttpRequest, FromRequest, Result}; use actix_web::middleware::{Middleware, Started}; -use actix_web_httpauth::BasicAuth; +use actix_web_httpauth::basic::{BasicAuth, Config}; struct AuthMiddleware; impl Middleware for AuthMiddleware { fn start(&self, req: &mut HttpRequest) -> Result { - let auth = BasicAuth::extract(&req)?; + let mut config = Config::default(); + config.realm("Restricted area".to_string()); + let auth = BasicAuth::from_request(&req, &config)?; // Please note that this is only an example, // do not ever hardcode your credentials! if auth.username == "root" && auth.password == "pass" { Ok(Started::Done) } else { - let response = req.build_response(StatusCode::UNAUTHORIZED) - .header("WWW-Authenticate", "Basic") - .finish(); + let response = BasicAuth::error_response(&config); Ok(Started::Response(response)) } } @@ -32,8 +31,8 @@ fn index(auth: BasicAuth) -> String { fn main() { server::new(|| App::new() - // Comment line below to pass authentication handling - // directly to `index` handler. + // Comment the `.middleware()` line and let `BasicAuth` extractor + // in the `index` handler do the authentication routine .middleware(AuthMiddleware) .resource("/", |r| r.with(index))) .bind("127.0.0.1:8088").unwrap() diff --git a/src/basic/config.rs b/src/basic/config.rs new file mode 100644 index 000000000..298ffac3e --- /dev/null +++ b/src/basic/config.rs @@ -0,0 +1,61 @@ +use std::default::Default; + +use bytes::Bytes; +use percent_encoding; +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue}; + +use challenge::Challenge; + +/// Challenge configuration for [BasicAuth](./struct.BasicAuth.html) extractor. +#[derive(Debug, Clone)] +pub struct Config { + // "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A + realm: Option, +} + +impl Config { + pub fn realm(&mut self, value: String) -> &mut Self { + self.realm = Some(value); + self + } + + fn as_bytes(&self) -> Bytes { + let mut bytes = Bytes::from_static(b"Basic"); + if let Some(ref realm) = self.realm { + bytes.extend_from_slice(b" realm=\""); + let realm = percent_encoding::utf8_percent_encode(realm, percent_encoding::SIMPLE_ENCODE_SET); + for part in realm { + bytes.extend_from_slice(part.as_bytes()); + } + bytes.extend_from_slice(b"\""); + } + + bytes + } +} + +impl IntoHeaderValue for Config { + type Error = InvalidHeaderValue; + + fn try_into(self) -> Result::Error> { + HeaderValue::from_bytes(&self.as_bytes()) + } +} + +impl<'a> IntoHeaderValue for &'a Config { + type Error = InvalidHeaderValue; + + fn try_into(self) -> Result::Error> { + HeaderValue::from_bytes(&self.as_bytes()) + } +} + +impl Default for Config { + fn default() -> Self { + Config { + realm: None, + } + } +} + +impl Challenge for Config {} diff --git a/src/basic/mod.rs b/src/basic/mod.rs new file mode 100644 index 000000000..c2c508d0a --- /dev/null +++ b/src/basic/mod.rs @@ -0,0 +1,101 @@ +use std::string; +use std::convert::From; + +use base64; +use actix_web::{HttpRequest, HttpMessage, HttpResponse, FromRequest, ResponseError}; +use actix_web::http::header; + +mod config; + +use errors::Error; +pub use self::config::Config; + +/// Extractor for `Authorization: Basic {payload}` HTTP request header. +/// +/// If header is not present or malformed, `HTTP 401` response will be returned. +/// See [Config](./struct.Config.html) struct also. +/// +/// # Example +/// +/// As a handler-level extractor: +/// +/// ```rust +/// use actix_web_httpauth::basic::BasicAuth; +/// +/// pub fn handler(auth: BasicAuth) -> String { +/// format!("Hello, {}", auth.username) +/// } +/// ``` +/// +/// See `examples/basic.rs` file in sources +#[derive(Debug, PartialEq)] +pub struct BasicAuth { + pub username: String, + pub password: String, +} + +impl BasicAuth { + pub fn error_response(cfg: &Config) -> HttpResponse { + Error::new(cfg.clone()).error_response() + } + + fn parse(req: &HttpRequest) -> Result { + let header = req.headers().get(header::AUTHORIZATION) + .ok_or(ParseError)? + .to_str()?; + let mut parts = header.splitn(2, ' '); + + // Authorization mechanism + match parts.next() { + Some(mechanism) if mechanism == "Basic" => (), + _ => return Err(ParseError), + } + + // Authorization payload + let payload = parts.next().ok_or(ParseError)?; + let payload = base64::decode(payload)?; + let payload = String::from_utf8(payload)?; + let mut parts = payload.splitn(2, ':'); + let user = parts.next().ok_or(ParseError)?; + let password = parts.next().ok_or(ParseError)?; + + Ok(BasicAuth{ + username: user.to_string(), + password: password.to_string(), + }) + } +} + + +impl FromRequest for BasicAuth { + type Config = Config; + type Result = Result; + + fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { + BasicAuth::parse(req).map_err(|_| Error::new(cfg.clone())) + } +} + +#[derive(Debug)] +struct ParseError; + +impl From for ParseError { + fn from(_: base64::DecodeError) -> Self { + Self{} + } +} + +impl From for ParseError { + fn from(_: header::ToStrError) -> Self { + Self{} + } +} + +impl From for ParseError { + fn from(_: string::FromUtf8Error) -> Self { + Self{} + } +} + +#[cfg(test)] +mod tests; diff --git a/src/basic/tests.rs b/src/basic/tests.rs new file mode 100644 index 000000000..25b9436fb --- /dev/null +++ b/src/basic/tests.rs @@ -0,0 +1,56 @@ +use base64; +use actix_web::FromRequest; +use actix_web::test::TestRequest; + +use super::BasicAuth; + +#[test] +fn test_valid_auth() { + let value = format!("Basic {}", base64::encode("user:pass")); + let req = TestRequest::with_header("Authorization", value).finish(); + let auth = BasicAuth::extract(&req); + + assert!(auth.is_ok()); + let auth = auth.unwrap(); + assert_eq!(auth.username, "user".to_string()); + assert_eq!(auth.password, "pass".to_string()); +} + +#[test] +fn test_missing_header() { + let req = TestRequest::default().finish(); + let auth = BasicAuth::extract(&req); + + assert!(auth.is_err()); +} + +#[test] +fn test_invalid_mechanism() { + let value = format!("Digest {}", base64::encode("user:pass")); + let req = TestRequest::with_header("Authorization", value).finish(); + let auth = BasicAuth::extract(&req); + + assert!(auth.is_err()); +} + +#[test] +fn test_invalid_format() { + let value = format!("Basic {}", base64::encode("user")); + let req = TestRequest::with_header("Authorization", value).finish(); + let auth = BasicAuth::extract(&req); + + assert!(auth.is_err()); +} + +#[test] +fn test_user_without_password() { + let value = format!("Basic {}", base64::encode("user:")); + let req = TestRequest::with_header("Authorization", value).finish(); + let auth = BasicAuth::extract(&req); + + assert!(auth.is_ok()); + assert_eq!(auth.unwrap(), BasicAuth { + username: "user".to_string(), + password: "".to_string(), + }) +} diff --git a/src/challenge.rs b/src/challenge.rs new file mode 100644 index 000000000..de2eb566b --- /dev/null +++ b/src/challenge.rs @@ -0,0 +1,6 @@ +use std::fmt::Debug; +use std::default::Default; + +use actix_web::http::header::IntoHeaderValue; + +pub trait Challenge: 'static + Debug + Clone + Send + Sync + IntoHeaderValue + Default {} \ No newline at end of file diff --git a/src/errors.rs b/src/errors.rs index 59ccd3db5..22be9ad6d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,65 +1,41 @@ use std::fmt; -use std::error::Error; -use std::string; +use std::error::Error as StdError; -use base64; use actix_web::HttpResponse; use actix_web::error::ResponseError; use actix_web::http::{StatusCode, header}; -#[derive(Debug, PartialEq)] -pub enum AuthError { - HeaderMissing, // HTTP 401 - // TODO: Ensure that 401 should be returned if not a `Basic` mechanism is received - InvalidMechanism, // HTTP 401 ? - HeaderMalformed, // HTTP 400 +use basic::Config; + +#[derive(Debug)] +pub struct Error { + challenge: Config, } -impl fmt::Display for AuthError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.description()) - } -} - -impl Error for AuthError { - fn description(&self) -> &str { - match *self { - AuthError::HeaderMissing => "HTTP 'Authorization' header is missing", - AuthError::InvalidMechanism => "Wrong mechanism for a HTTP 'Authorization' header, expected 'Basic'", - AuthError::HeaderMalformed => "Malformed HTTP 'Authorization' header", +impl Error { + pub fn new(config: Config) -> Error { + Error { + challenge: config, } } } -impl From for AuthError { - fn from(_: header::ToStrError) -> Self { - AuthError::HeaderMalformed - } -} - -impl From for AuthError { - fn from(_: base64::DecodeError) -> Self { - AuthError::HeaderMalformed - } -} - -impl From for AuthError { - fn from(_: string::FromUtf8Error) -> Self { - AuthError::HeaderMalformed - } -} - -impl ResponseError for AuthError { +impl ResponseError for Error { fn error_response(&self) -> HttpResponse { - let status = match *self { - AuthError::HeaderMissing => StatusCode::UNAUTHORIZED, - AuthError::InvalidMechanism => StatusCode::UNAUTHORIZED, - AuthError::HeaderMalformed => StatusCode::BAD_REQUEST, - }; - - HttpResponse::build(status) - .header("WWW-Authenticate", "Basic") + HttpResponse::build(StatusCode::UNAUTHORIZED) + .header(header::WWW_AUTHENTICATE, &self.challenge) .finish() } - +} + +impl StdError for Error { + fn description(&self) -> &str { + "Unauthorized request" + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.description()) + } } diff --git a/src/lib.rs b/src/lib.rs index 1b1d0654b..40aa75125 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,13 @@ +//! HTTP authorization routines for [actix-web](https://github.com/actix/actix-web) framework. +//! +//! Currently supported schemas: +//! * Basic ([RFC-7617](https://tools.ietf.org/html/rfc7617)) + +extern crate bytes; +extern crate percent_encoding; extern crate actix_web; extern crate base64; -mod schemes; mod errors; - -pub use schemes::*; -pub use errors::AuthError; +mod challenge; +pub mod basic; From 09f9a82a0443cf58d141e1d045ff02676bafa7cc Mon Sep 17 00:00:00 2001 From: svartalf Date: Wed, 23 May 2018 15:37:44 +0300 Subject: [PATCH 07/36] Cleaning up --- src/basic/config.rs | 4 -- src/challenge.rs | 6 --- src/lib.rs | 1 - src/schemes/basic.rs | 122 ------------------------------------------- src/schemes/mod.rs | 3 -- 5 files changed, 136 deletions(-) delete mode 100644 src/challenge.rs delete mode 100644 src/schemes/basic.rs delete mode 100644 src/schemes/mod.rs diff --git a/src/basic/config.rs b/src/basic/config.rs index 298ffac3e..a11816c7a 100644 --- a/src/basic/config.rs +++ b/src/basic/config.rs @@ -4,8 +4,6 @@ use bytes::Bytes; use percent_encoding; use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue}; -use challenge::Challenge; - /// Challenge configuration for [BasicAuth](./struct.BasicAuth.html) extractor. #[derive(Debug, Clone)] pub struct Config { @@ -57,5 +55,3 @@ impl Default for Config { } } } - -impl Challenge for Config {} diff --git a/src/challenge.rs b/src/challenge.rs deleted file mode 100644 index de2eb566b..000000000 --- a/src/challenge.rs +++ /dev/null @@ -1,6 +0,0 @@ -use std::fmt::Debug; -use std::default::Default; - -use actix_web::http::header::IntoHeaderValue; - -pub trait Challenge: 'static + Debug + Clone + Send + Sync + IntoHeaderValue + Default {} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 40aa75125..bcd182dcd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,5 +9,4 @@ extern crate actix_web; extern crate base64; mod errors; -mod challenge; pub mod basic; diff --git a/src/schemes/basic.rs b/src/schemes/basic.rs deleted file mode 100644 index 89990c5bf..000000000 --- a/src/schemes/basic.rs +++ /dev/null @@ -1,122 +0,0 @@ -use base64; -use actix_web::{HttpRequest, HttpMessage, FromRequest}; - -use errors::AuthError; - -/// Extractor for `Authorization: Basic {payload}` HTTP request header. -/// -/// If header is not present, HTTP 401 will be returned. -/// -/// # Example -/// -/// ```rust -/// use actix_web_httpauth::BasicAuth; -/// -/// pub fn handler(auth: BasicAuth) -> String { -/// format!("Hello, {}", auth.username) -/// } -/// ``` -#[derive(Debug, PartialEq)] -pub struct BasicAuth { - pub username: String, - pub password: String, -} - - -impl FromRequest for BasicAuth { - type Config = (); - type Result = Result; - - fn from_request(req: &HttpRequest, _cfg: &>::Config) -> >::Result { - let header = req.headers().get("Authorization") - .ok_or(AuthError::HeaderMissing)? - .to_str()?; - let mut parts = header.splitn(2, ' '); - - // Authorization mechanism - match parts.next() { - None => return Err(AuthError::InvalidMechanism), - Some(mechanism) if mechanism != "Basic" => return Err(AuthError::InvalidMechanism), - _ => () - } - - // Authorization payload - let payload = parts.next().ok_or(AuthError::HeaderMalformed)?; - let payload = base64::decode(payload)?; - let payload = String::from_utf8(payload)?; - let mut parts = payload.splitn(2, ':'); - let user = parts.next().ok_or(AuthError::HeaderMalformed)?; - let password = parts.next().ok_or(AuthError::HeaderMalformed)?; - - Ok(BasicAuth{ - username: user.to_string(), - password: password.to_string(), - }) - } -} - -#[cfg(test)] -mod tests { - use base64; - use actix_web::FromRequest; - use actix_web::test::TestRequest; - - use super::{BasicAuth, AuthError}; - - #[test] - fn test_valid_auth() { - let value = format!("Basic {}", base64::encode("user:pass")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_ok()); - let auth = auth.unwrap(); - assert_eq!(auth.username, "user".to_string()); - assert_eq!(auth.password, "pass".to_string()); - } - - #[test] - fn test_missing_header() { - let req = TestRequest::default().finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_err()); - let err = auth.err().unwrap(); - assert_eq!(err, AuthError::HeaderMissing); - } - - #[test] - fn test_invalid_mechanism() { - let value = format!("Digest {}", base64::encode("user:pass")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_err()); - let err = auth.err().unwrap(); - assert_eq!(err, AuthError::InvalidMechanism); - } - - #[test] - fn test_invalid_format() { - let value = format!("Basic {}", base64::encode("user")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_err()); - let err = auth.err().unwrap(); - assert_eq!(err, AuthError::HeaderMalformed); - } - - #[test] - fn test_user_without_password() { - let value = format!("Basic {}", base64::encode("user:")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_ok()); - assert_eq!(auth.unwrap(), BasicAuth { - username: "user".to_string(), - password: "".to_string(), - }) - } -} diff --git a/src/schemes/mod.rs b/src/schemes/mod.rs deleted file mode 100644 index 43d195913..000000000 --- a/src/schemes/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod basic; - -pub use self::basic::BasicAuth; From eee5365e0b26b41bf91e5e969af6f378e6c58ab7 Mon Sep 17 00:00:00 2001 From: svartalf Date: Wed, 23 May 2018 23:17:40 +0300 Subject: [PATCH 08/36] Bumping crate version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8aac469dd..e88f6bd1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.0.1" +version = "0.0.2" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -19,4 +19,4 @@ travis-ci = { repository = "svartalf/actix-web-httpauth", branch = "master" } actix-web = "0.6" base64 = "0.9" percent-encoding = "1.0.1" -bytes = "0.4.7" \ No newline at end of file +bytes = "0.4.7" From 716bffeb8e32c6432095fdaac46d684767052267 Mon Sep 17 00:00:00 2001 From: svartalf Date: Wed, 30 May 2018 16:43:39 +0300 Subject: [PATCH 09/36] Bearer auth --- .travis.yml | 1 + Cargo.toml | 15 +- README.md | 1 + examples/basic.rs | 40 ---- examples/extractor_basic.rs | 36 ++++ examples/extractor_bearer.rs | 40 ++++ examples/header_www_authenticate_basic.rs | 25 +++ examples/header_www_authenticate_bearer.rs | 29 +++ src/basic/config.rs | 57 ------ src/basic/mod.rs | 101 --------- src/basic/tests.rs | 56 ----- src/errors.rs | 41 ---- src/extractors/basic.rs | 80 ++++++++ src/extractors/bearer.rs | 101 +++++++++ src/extractors/config.rs | 15 ++ src/extractors/errors.rs | 63 ++++++ src/extractors/mod.rs | 6 + src/headers/authorization/errors.rs | 70 +++++++ src/headers/authorization/header.rs | 85 ++++++++ src/headers/authorization/mod.rs | 9 + src/headers/authorization/scheme/basic.rs | 191 ++++++++++++++++++ src/headers/authorization/scheme/bearer.rs | 111 ++++++++++ src/headers/authorization/scheme/mod.rs | 13 ++ src/headers/mod.rs | 2 + .../www_authenticate/challenge/basic.rs | 91 +++++++++ .../www_authenticate/challenge/bearer.rs | 136 +++++++++++++ src/headers/www_authenticate/challenge/mod.rs | 12 ++ src/headers/www_authenticate/header.rs | 49 +++++ src/headers/www_authenticate/mod.rs | 7 + src/lib.rs | 17 +- 30 files changed, 1192 insertions(+), 308 deletions(-) delete mode 100644 examples/basic.rs create mode 100644 examples/extractor_basic.rs create mode 100644 examples/extractor_bearer.rs create mode 100644 examples/header_www_authenticate_basic.rs create mode 100644 examples/header_www_authenticate_bearer.rs delete mode 100644 src/basic/config.rs delete mode 100644 src/basic/mod.rs delete mode 100644 src/basic/tests.rs delete mode 100644 src/errors.rs create mode 100644 src/extractors/basic.rs create mode 100644 src/extractors/bearer.rs create mode 100644 src/extractors/config.rs create mode 100644 src/extractors/errors.rs create mode 100644 src/extractors/mod.rs create mode 100644 src/headers/authorization/errors.rs create mode 100644 src/headers/authorization/header.rs create mode 100644 src/headers/authorization/mod.rs create mode 100644 src/headers/authorization/scheme/basic.rs create mode 100644 src/headers/authorization/scheme/bearer.rs create mode 100644 src/headers/authorization/scheme/mod.rs create mode 100644 src/headers/mod.rs create mode 100644 src/headers/www_authenticate/challenge/basic.rs create mode 100644 src/headers/www_authenticate/challenge/bearer.rs create mode 100644 src/headers/www_authenticate/challenge/mod.rs create mode 100644 src/headers/www_authenticate/header.rs create mode 100644 src/headers/www_authenticate/mod.rs diff --git a/.travis.yml b/.travis.yml index 8c91a7415..45793de3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: rust +cache: cargo rust: - stable - beta diff --git a/Cargo.toml b/Cargo.toml index e88f6bd1e..3bb8ce0f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.0.2" +version = "0.0.3" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -12,11 +12,14 @@ categories = ["web-programming::http-server"] license = "MIT/Apache-2.0" exclude = [".travis.yml", ".gitignore"] -[badges] -travis-ci = { repository = "svartalf/actix-web-httpauth", branch = "master" } - [dependencies] actix-web = "0.6" +bytes = "0.4" base64 = "0.9" -percent-encoding = "1.0.1" -bytes = "0.4.7" + +[features] +default = [] +nightly = [] + +[badges] +travis-ci = { repository = "svartalf/actix-web-httpauth", branch = "master" } diff --git a/README.md b/README.md index 7b199822a..e64d18105 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,4 @@ and can be used both in middlewares and request handlers, check the `examples/` ## Supported schemes * [Basic](https://tools.ietf.org/html/rfc7617) + * [Bearer](https://tools.ietf.org/html/rfc6750) diff --git a/examples/basic.rs b/examples/basic.rs deleted file mode 100644 index 3a32eb938..000000000 --- a/examples/basic.rs +++ /dev/null @@ -1,40 +0,0 @@ -extern crate actix_web; -extern crate actix_web_httpauth; - -use actix_web::{server, App, HttpRequest, FromRequest, Result}; -use actix_web::middleware::{Middleware, Started}; -use actix_web_httpauth::basic::{BasicAuth, Config}; - -struct AuthMiddleware; - -impl Middleware for AuthMiddleware { - fn start(&self, req: &mut HttpRequest) -> Result { - let mut config = Config::default(); - config.realm("Restricted area".to_string()); - let auth = BasicAuth::from_request(&req, &config)?; - - // Please note that this is only an example, - // do not ever hardcode your credentials! - if auth.username == "root" && auth.password == "pass" { - Ok(Started::Done) - } else { - let response = BasicAuth::error_response(&config); - Ok(Started::Response(response)) - } - } - -} - -fn index(auth: BasicAuth) -> String { - format!("Hello, {}", auth.username) -} - -fn main() { - server::new(|| App::new() - // Comment the `.middleware()` line and let `BasicAuth` extractor - // in the `index` handler do the authentication routine - .middleware(AuthMiddleware) - .resource("/", |r| r.with(index))) - .bind("127.0.0.1:8088").unwrap() - .run(); -} diff --git a/examples/extractor_basic.rs b/examples/extractor_basic.rs new file mode 100644 index 000000000..8e63ae397 --- /dev/null +++ b/examples/extractor_basic.rs @@ -0,0 +1,36 @@ +extern crate actix_web; +extern crate actix_web_httpauth; + +use actix_web::{server, App, Result, HttpRequest, FromRequest}; +use actix_web::middleware::{Middleware, Started}; +use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; +use actix_web_httpauth::extractors::AuthenticationError; + +struct Auth; + +impl Middleware for Auth { + fn start(&self, req: &mut HttpRequest) -> Result { + let mut config = Config::default(); + config.realm("WallyWorld"); + let auth = BasicAuth::from_request(&req, &config)?; + + if auth.username() == "Aladdin" && auth.password() == Some("open sesame") { + Ok(Started::Done) + } else { + Err(AuthenticationError::from(config).into()) + } + } +} + +fn index(_req: HttpRequest) -> String { + "Hello, authorized user!".to_string() +} + +fn main() { + server::new(|| App::new() + .middleware(Auth) + .resource("/", |r| r.with(index)) + ) + .bind("127.0.0.1:8088").unwrap() + .run(); +} diff --git a/examples/extractor_bearer.rs b/examples/extractor_bearer.rs new file mode 100644 index 000000000..145abecca --- /dev/null +++ b/examples/extractor_bearer.rs @@ -0,0 +1,40 @@ +extern crate actix_web; +extern crate actix_web_httpauth; + +use actix_web::{server, App, HttpRequest, Result, FromRequest}; +use actix_web_httpauth::extractors::AuthenticationError; +use actix_web_httpauth::extractors::bearer::{BearerAuth, Config, Error}; +use actix_web::middleware::{Middleware, Started}; + +struct Auth; + +impl Middleware for Auth { + fn start(&self, req: &mut HttpRequest) -> Result { + let mut config = Config::default(); + config.realm("Restricted area"); + config.scope("openid profile email"); + let auth = BearerAuth::from_request(&req, &config)?; + + if auth.token() == "mF_9.B5f-4.1JqM" { + Ok(Started::Done) + } else { + Err(AuthenticationError::from(config) + .with_error(Error::InvalidToken) + .into()) + } + } + +} + +fn index(_req: HttpRequest) -> String { + "Hello, authorized user!".to_string() +} + +fn main() { + server::new(|| App::new() + .middleware(Auth) + .resource("/", |r| r.with(index)) + ) + .bind("127.0.0.1:8088").unwrap() + .run(); +} diff --git a/examples/header_www_authenticate_basic.rs b/examples/header_www_authenticate_basic.rs new file mode 100644 index 000000000..031948b20 --- /dev/null +++ b/examples/header_www_authenticate_basic.rs @@ -0,0 +1,25 @@ +extern crate actix_web; +extern crate actix_web_httpauth; + +use actix_web::{server, App, HttpRequest, HttpResponse}; +use actix_web::http::StatusCode; +use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate}; +use actix_web_httpauth::headers::www_authenticate::basic::Basic; + + +fn index(req: HttpRequest) -> HttpResponse { + let challenge = Basic { + realm: Some("Restricted area".to_string()), + }; + + req.build_response(StatusCode::UNAUTHORIZED) + .set(WWWAuthenticate(challenge)) + .finish() +} + +fn main() { + server::new(|| App::new() + .resource("/", |r| r.with(index))) + .bind("127.0.0.1:8088").unwrap() + .run(); +} diff --git a/examples/header_www_authenticate_bearer.rs b/examples/header_www_authenticate_bearer.rs new file mode 100644 index 000000000..65d24b822 --- /dev/null +++ b/examples/header_www_authenticate_bearer.rs @@ -0,0 +1,29 @@ +extern crate actix_web; +extern crate actix_web_httpauth; + +use actix_web::{server, App, HttpRequest, HttpResponse}; +use actix_web::http::StatusCode; +use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate}; +use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer, Error}; + + +fn index(req: HttpRequest) -> HttpResponse { + let challenge = Bearer { + realm: Some("example".to_string()), + scope: Some("openid profile email".to_string()), + error: Some(Error::InvalidToken), + error_description: Some("The access token expired".to_string()), + error_uri: Some("http://example.org".to_string()), + }; + + req.build_response(StatusCode::UNAUTHORIZED) + .set(WWWAuthenticate(challenge)) + .finish() +} + +fn main() { + server::new(|| App::new() + .resource("/", |r| r.with(index))) + .bind("127.0.0.1:8088").unwrap() + .run(); +} diff --git a/src/basic/config.rs b/src/basic/config.rs deleted file mode 100644 index a11816c7a..000000000 --- a/src/basic/config.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::default::Default; - -use bytes::Bytes; -use percent_encoding; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue}; - -/// Challenge configuration for [BasicAuth](./struct.BasicAuth.html) extractor. -#[derive(Debug, Clone)] -pub struct Config { - // "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A - realm: Option, -} - -impl Config { - pub fn realm(&mut self, value: String) -> &mut Self { - self.realm = Some(value); - self - } - - fn as_bytes(&self) -> Bytes { - let mut bytes = Bytes::from_static(b"Basic"); - if let Some(ref realm) = self.realm { - bytes.extend_from_slice(b" realm=\""); - let realm = percent_encoding::utf8_percent_encode(realm, percent_encoding::SIMPLE_ENCODE_SET); - for part in realm { - bytes.extend_from_slice(part.as_bytes()); - } - bytes.extend_from_slice(b"\""); - } - - bytes - } -} - -impl IntoHeaderValue for Config { - type Error = InvalidHeaderValue; - - fn try_into(self) -> Result::Error> { - HeaderValue::from_bytes(&self.as_bytes()) - } -} - -impl<'a> IntoHeaderValue for &'a Config { - type Error = InvalidHeaderValue; - - fn try_into(self) -> Result::Error> { - HeaderValue::from_bytes(&self.as_bytes()) - } -} - -impl Default for Config { - fn default() -> Self { - Config { - realm: None, - } - } -} diff --git a/src/basic/mod.rs b/src/basic/mod.rs deleted file mode 100644 index c2c508d0a..000000000 --- a/src/basic/mod.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::string; -use std::convert::From; - -use base64; -use actix_web::{HttpRequest, HttpMessage, HttpResponse, FromRequest, ResponseError}; -use actix_web::http::header; - -mod config; - -use errors::Error; -pub use self::config::Config; - -/// Extractor for `Authorization: Basic {payload}` HTTP request header. -/// -/// If header is not present or malformed, `HTTP 401` response will be returned. -/// See [Config](./struct.Config.html) struct also. -/// -/// # Example -/// -/// As a handler-level extractor: -/// -/// ```rust -/// use actix_web_httpauth::basic::BasicAuth; -/// -/// pub fn handler(auth: BasicAuth) -> String { -/// format!("Hello, {}", auth.username) -/// } -/// ``` -/// -/// See `examples/basic.rs` file in sources -#[derive(Debug, PartialEq)] -pub struct BasicAuth { - pub username: String, - pub password: String, -} - -impl BasicAuth { - pub fn error_response(cfg: &Config) -> HttpResponse { - Error::new(cfg.clone()).error_response() - } - - fn parse(req: &HttpRequest) -> Result { - let header = req.headers().get(header::AUTHORIZATION) - .ok_or(ParseError)? - .to_str()?; - let mut parts = header.splitn(2, ' '); - - // Authorization mechanism - match parts.next() { - Some(mechanism) if mechanism == "Basic" => (), - _ => return Err(ParseError), - } - - // Authorization payload - let payload = parts.next().ok_or(ParseError)?; - let payload = base64::decode(payload)?; - let payload = String::from_utf8(payload)?; - let mut parts = payload.splitn(2, ':'); - let user = parts.next().ok_or(ParseError)?; - let password = parts.next().ok_or(ParseError)?; - - Ok(BasicAuth{ - username: user.to_string(), - password: password.to_string(), - }) - } -} - - -impl FromRequest for BasicAuth { - type Config = Config; - type Result = Result; - - fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { - BasicAuth::parse(req).map_err(|_| Error::new(cfg.clone())) - } -} - -#[derive(Debug)] -struct ParseError; - -impl From for ParseError { - fn from(_: base64::DecodeError) -> Self { - Self{} - } -} - -impl From for ParseError { - fn from(_: header::ToStrError) -> Self { - Self{} - } -} - -impl From for ParseError { - fn from(_: string::FromUtf8Error) -> Self { - Self{} - } -} - -#[cfg(test)] -mod tests; diff --git a/src/basic/tests.rs b/src/basic/tests.rs deleted file mode 100644 index 25b9436fb..000000000 --- a/src/basic/tests.rs +++ /dev/null @@ -1,56 +0,0 @@ -use base64; -use actix_web::FromRequest; -use actix_web::test::TestRequest; - -use super::BasicAuth; - -#[test] -fn test_valid_auth() { - let value = format!("Basic {}", base64::encode("user:pass")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_ok()); - let auth = auth.unwrap(); - assert_eq!(auth.username, "user".to_string()); - assert_eq!(auth.password, "pass".to_string()); -} - -#[test] -fn test_missing_header() { - let req = TestRequest::default().finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_err()); -} - -#[test] -fn test_invalid_mechanism() { - let value = format!("Digest {}", base64::encode("user:pass")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_err()); -} - -#[test] -fn test_invalid_format() { - let value = format!("Basic {}", base64::encode("user")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_err()); -} - -#[test] -fn test_user_without_password() { - let value = format!("Basic {}", base64::encode("user:")); - let req = TestRequest::with_header("Authorization", value).finish(); - let auth = BasicAuth::extract(&req); - - assert!(auth.is_ok()); - assert_eq!(auth.unwrap(), BasicAuth { - username: "user".to_string(), - password: "".to_string(), - }) -} diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index 22be9ad6d..000000000 --- a/src/errors.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::fmt; -use std::error::Error as StdError; - -use actix_web::HttpResponse; -use actix_web::error::ResponseError; -use actix_web::http::{StatusCode, header}; - -use basic::Config; - -#[derive(Debug)] -pub struct Error { - challenge: Config, -} - -impl Error { - pub fn new(config: Config) -> Error { - Error { - challenge: config, - } - } -} - -impl ResponseError for Error { - fn error_response(&self) -> HttpResponse { - HttpResponse::build(StatusCode::UNAUTHORIZED) - .header(header::WWW_AUTHENTICATE, &self.challenge) - .finish() - } -} - -impl StdError for Error { - fn description(&self) -> &str { - "Unauthorized request" - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.description()) - } -} diff --git a/src/extractors/basic.rs b/src/extractors/basic.rs new file mode 100644 index 000000000..484058bb6 --- /dev/null +++ b/src/extractors/basic.rs @@ -0,0 +1,80 @@ +use std::default::Default; + +use actix_web::{HttpRequest, FromRequest}; +use actix_web::http::header::Header; + +use headers::authorization::{Authorization, Basic}; +use headers::www_authenticate::basic::Basic as Challenge; +use super::errors::AuthenticationError; +use super::config::ExtractorConfig; + +/// [`BasicAuth`](./struct.BasicAuth.html) extractor configuration, +/// used for `WWW-Authenticate` header later. +#[derive(Debug, Clone)] +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) -> &mut Config { + self.0.realm = Some(value.into()); + self + } +} + +impl ExtractorConfig for Config { + type Inner = Challenge; + + fn into_inner(self) -> Self::Inner { + self.0 + } +} + +impl Default for Config { + fn default() -> Self { + Config(Challenge::default()) + } +} + +/// Extractor for HTTP Basic auth +/// +/// # Example +/// +/// ```rust +/// # extern crate actix_web; +/// # extern crate actix_web_httpauth; +/// use actix_web::Result; +/// use actix_web_httpauth::extractors::basic::BasicAuth; +/// +/// fn index(auth: BasicAuth) -> Result { +/// Ok(format!("Hello, {}!", auth.username())) +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct BasicAuth(Basic); + +impl BasicAuth { + pub fn username(&self) -> &str { + self.0.username.as_str() + } + + pub fn password(&self) -> Option<&str> { + match self.0.password { + None => None, + Some(ref pwd) => Some(pwd.as_str()) + } + } +} + +impl FromRequest for BasicAuth { + type Config = Config; + type Result = Result>; + + fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { + Authorization::::parse(req) + .map(|auth| BasicAuth(auth.into_inner())) + .map_err(|_| AuthenticationError::new(cfg.0.clone())) + } +} diff --git a/src/extractors/bearer.rs b/src/extractors/bearer.rs new file mode 100644 index 000000000..b3c9a5182 --- /dev/null +++ b/src/extractors/bearer.rs @@ -0,0 +1,101 @@ +use std::default::Default; + +use actix_web::{HttpRequest, FromRequest}; +use actix_web::http::header::Header; + +use headers::authorization; +use headers::www_authenticate::bearer; +pub use headers::www_authenticate::bearer::Error; +use super::errors::AuthenticationError; +use super::config::ExtractorConfig; + +/// [BearerAuth](./struct/BearerAuth.html) extractor configuration. +#[derive(Debug, Clone)] +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) -> &mut 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) -> &mut Config { + self.0.realm = Some(value.into()); + self + } +} + +impl ExtractorConfig for Config { + type Inner = bearer::Bearer; + + fn into_inner(self) -> Self::Inner { + self.0 + } +} + +impl Default for Config { + fn default() -> Self { + Config(bearer::Bearer::default()) + } +} + +/// Extractor for HTTP Bearer auth +/// +/// # Example +/// +/// ```rust +/// # extern crate actix_web; +/// # extern crate actix_web_httpauth; +/// use actix_web::Result; +/// use actix_web_httpauth::extractors::bearer::BearerAuth; +/// +/// fn index(auth: BearerAuth) -> Result { +/// Ok(format!("Hello, user with token {}!", auth.token())) +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct BearerAuth(authorization::Bearer); + +impl BearerAuth { + pub fn token(&self) -> &str { + self.0.token.as_str() + } +} + +impl FromRequest for BearerAuth { + type Config = Config; + type Result = Result>; + + fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { + authorization::Authorization::::parse(req) + .map(|auth| BearerAuth(auth.into_inner())) + .map_err(|_| AuthenticationError::new(cfg.0.clone())) + } +} + +/// Extended error customization for HTTP `Bearer` auth. +impl AuthenticationError { + pub fn with_error(mut self, kind: Error) -> Self { + *self.status_code_mut() = kind.status_code(); + self.challenge_mut().error = Some(kind); + self + } + + pub fn with_error_description>(mut self, desc: T) -> Self { + self.challenge_mut().error_description = Some(desc.into()); + self + } + + pub fn with_error_uri>(mut self, uri: T) -> Self { + self.challenge_mut().error_uri = Some(uri.into()); + self + } +} diff --git a/src/extractors/config.rs b/src/extractors/config.rs new file mode 100644 index 000000000..9337bffa5 --- /dev/null +++ b/src/extractors/config.rs @@ -0,0 +1,15 @@ +use headers::www_authenticate::Challenge; + +use super::AuthenticationError; + +pub trait ExtractorConfig { + type Inner: Challenge; + + fn into_inner(self) -> Self::Inner; +} + +impl From for AuthenticationError<::Inner> where T: ExtractorConfig { + fn from(config: T) -> Self { + AuthenticationError::new(config.into_inner()) + } +} diff --git a/src/extractors/errors.rs b/src/extractors/errors.rs new file mode 100644 index 000000000..3d161333b --- /dev/null +++ b/src/extractors/errors.rs @@ -0,0 +1,63 @@ +use std::str; +use std::fmt; +use std::error::Error; + +use actix_web::{HttpResponse, ResponseError}; +use actix_web::http::StatusCode; + +use headers::www_authenticate::{WWWAuthenticate}; +use headers::www_authenticate::Challenge; + +/// Authentication error returned by Auth extractor. +/// +/// 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 { + pub fn new(challenge: C) -> AuthenticationError { + AuthenticationError { + challenge, + status_code: StatusCode::UNAUTHORIZED, + } + } + + pub fn challenge_mut(&mut self) -> &mut C { + &mut self.challenge + } + + 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 { + let bytes = self.challenge.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 Error for AuthenticationError { + fn description(&self) -> &str { + unimplemented!() + } +} + +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/src/extractors/mod.rs b/src/extractors/mod.rs new file mode 100644 index 000000000..be364ab26 --- /dev/null +++ b/src/extractors/mod.rs @@ -0,0 +1,6 @@ +mod errors; +mod config; +pub mod basic; +pub mod bearer; + +pub use self::errors::AuthenticationError; diff --git a/src/headers/authorization/errors.rs b/src/headers/authorization/errors.rs new file mode 100644 index 000000000..abdb80f9b --- /dev/null +++ b/src/headers/authorization/errors.rs @@ -0,0 +1,70 @@ +use std::str; +use std::fmt; +use std::error::Error; +use std::convert::From; + +use base64; +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), + ToStrError(header::ToStrError), + Base64DecodeError(base64::DecodeError), + Utf8Error(str::Utf8Error), +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.description()) + } +} + +impl Error for ParseError { + fn description(&self) -> &str { + match self { + ParseError::Invalid => "Invalid header value", + ParseError::MissingScheme => "Missing authorization scheme", + ParseError::MissingField(_) => "Missing header field", + ParseError::ToStrError(e) => e.description(), + ParseError::Base64DecodeError(e) => e.description(), + ParseError::Utf8Error(e) => e.description(), + } + } + + fn cause(&self) -> Option<&Error> { + 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/src/headers/authorization/header.rs b/src/headers/authorization/header.rs new file mode 100644 index 000000000..ab801f3bf --- /dev/null +++ b/src/headers/authorization/header.rs @@ -0,0 +1,85 @@ +use std::ops; +use std::fmt; + +use actix_web::{HttpMessage}; +use actix_web::error::ParseError; +use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION}; + +use 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 +/// +/// ```rust +/// # extern crate actix_web; +/// # extern crate actix_web_httpauth; +/// +/// use actix_web::{HttpRequest, Result}; +/// use actix_web::http::header::Header; +/// use actix_web_httpauth::headers::authorization::{Authorization, Basic}; +/// +/// fn handler(req: HttpRequest) -> Result { +/// let auth = Authorization::::parse(&req)?; +/// +/// Ok(format!("Hello, {}!", auth.username)) +/// } +/// ``` +pub struct Authorization(S); + +impl Authorization { + pub fn into_inner(self) -> S { + 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) + } +} + +impl ops::Deref for Authorization { + type Target = S; + + fn deref(&self) -> &::Target { + &self.0 + } +} + +impl ops::DerefMut for Authorization { + fn deref_mut(&mut self) -> &mut ::Target { + &mut self.0 + } +} diff --git a/src/headers/authorization/mod.rs b/src/headers/authorization/mod.rs new file mode 100644 index 000000000..d1420a5dc --- /dev/null +++ b/src/headers/authorization/mod.rs @@ -0,0 +1,9 @@ +mod scheme; +mod header; +mod errors; + +pub use self::scheme::Scheme; +pub use self::scheme::basic::Basic; +pub use self::scheme::bearer::Bearer; +pub use self::errors::ParseError; +pub use self::header::Authorization; diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs new file mode 100644 index 000000000..449ea2ad6 --- /dev/null +++ b/src/headers/authorization/scheme/basic.rs @@ -0,0 +1,191 @@ +use std::str; +use std::fmt; + +use base64; +use bytes::{BufMut, BytesMut}; +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; + +use headers::authorization::Scheme; +use headers::authorization::errors::ParseError; + +/// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617) +#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] +pub struct Basic { + pub username: String, + pub password: Option, +} + +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 username = credentials.next() + .ok_or(ParseError::MissingField("username")) + .map(|username| username.to_string())?; + let password = credentials.next() + .ok_or(ParseError::MissingField("password")) + .map(|password| { + if password.is_empty() { + None + } else { + Some(password.to_string()) + } + })?; + + Ok(Basic{ + username, + password, + }) + } +} + +impl fmt::Debug for Basic { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_fmt(format_args!("Basic {}:******", self.username)) + } +} + +impl fmt::Display for Basic { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO: Display password also + f.write_fmt(format_args!("Basic {}:******", self.username)) + } +} + +impl IntoHeaderValue for Basic { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result::Error> { + let mut credentials = BytesMut::with_capacity( + self.username.len() + self.password.as_ref().map_or(0, |pwd| pwd.len()) + ); + credentials.put(&self.username); + credentials.put_u8(b':'); + if let Some(ref password) = self.password { + credentials.put(password); + } + + // 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("Basic "); + value.put(&encoded); + + HeaderValue::from_shared(value.freeze()) + } +} + +#[cfg(test)] +mod tests { + use actix_web::http::header::{HeaderValue, IntoHeaderValue}; + use super::{Scheme, Basic}; + + #[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.username, "Aladdin"); + assert_eq!(scheme.password, Some("open sesame".to_string())); + } + + #[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.username, "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 { + username: "Aladdin".to_string(), + password: Some("open sesame".to_string()), + }; + + let result = basic.try_into(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")); + } +} + + +#[cfg(all(test, feature = "nightly"))] +mod benches { + use test::Bencher; + + use actix_web::http::header::{HeaderValue, IntoHeaderValue}; + + use super::{Basic, Scheme}; + + #[bench] + fn bench_parsing(b: &mut Bencher) { + let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + b.iter(|| { + Basic::parse(&value) + }); + } + + #[bench] + fn bench_serializing(b: &mut Bencher) { + b.iter(|| { + let basic = Basic { + username: "Aladdin".to_string(), + password: Some("open sesame".to_string()), + }; + + basic.try_into() + }) + } +} diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs new file mode 100644 index 000000000..2a7257cd9 --- /dev/null +++ b/src/headers/authorization/scheme/bearer.rs @@ -0,0 +1,111 @@ +use std::fmt; + +use bytes::{BufMut, BytesMut}; +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; + +use headers::authorization::scheme::Scheme; +use headers::authorization::errors::ParseError; + +/// 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 { + pub token: String, +} + +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(), + }) + } +} + +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 = InvalidHeaderValueBytes; + + fn try_into(self) -> Result::Error> { + let mut buffer = BytesMut::with_capacity(7 + self.token.len()); + buffer.put("Bearer "); + buffer.put(self.token); + + HeaderValue::from_shared(buffer.freeze()) + } +} + +#[cfg(test)] +mod tests { + use actix_web::http::header::{HeaderValue, IntoHeaderValue}; + use super::{Scheme, Bearer}; + + #[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 { + token: "mF_9.B5f-4.1JqM".to_string(), + }; + + 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/src/headers/authorization/scheme/mod.rs b/src/headers/authorization/scheme/mod.rs new file mode 100644 index 000000000..42524129e --- /dev/null +++ b/src/headers/authorization/scheme/mod.rs @@ -0,0 +1,13 @@ +use std::fmt::{Debug, Display}; + +use actix_web::http::header::{IntoHeaderValue, HeaderValue}; + +pub mod basic; +pub mod bearer; + +use headers::authorization::errors::ParseError; + +/// Authentication scheme for [`Authorization`](./struct.Authorization.html) header. +pub trait Scheme: IntoHeaderValue + Debug + Display + Clone + Send + Sync { + fn parse(header: &HeaderValue) -> Result; +} diff --git a/src/headers/mod.rs b/src/headers/mod.rs new file mode 100644 index 000000000..0d0d12342 --- /dev/null +++ b/src/headers/mod.rs @@ -0,0 +1,2 @@ +pub mod authorization; +pub mod www_authenticate; diff --git a/src/headers/www_authenticate/challenge/basic.rs b/src/headers/www_authenticate/challenge/basic.rs new file mode 100644 index 000000000..492b2677c --- /dev/null +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -0,0 +1,91 @@ +use std::str; +use std::fmt; +use std::default::Default; + +use bytes::{BufMut, Bytes, BytesMut}; +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; + +use super::Challenge; + +/// Challenge for `WWW-Authenticate` header with HTTP Basic auth scheme, +/// described in [RFC 7617](https://tools.ietf.org/html/rfc7617) +#[derive(Debug, Clone)] +pub struct Basic { + // "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A + pub realm: Option, +} + +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("Basic"); + if let Some(ref realm) = self.realm { + buffer.put(" realm=\""); + buffer.put(realm); + buffer.put("\""); + } + + 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 = InvalidHeaderValueBytes; + + fn try_into(self) -> Result::Error> { + HeaderValue::from_shared(self.to_bytes()) + } +} + + +impl Default for Basic { + fn default() -> Self { + Self { + realm: None, + } + } +} + +#[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".to_string()), + }; + + let value = challenge.try_into(); + assert!(value.is_ok()); + let value = value.unwrap(); + assert_eq!(value, "Basic realm=\"Restricted area\""); + } +} diff --git a/src/headers/www_authenticate/challenge/bearer.rs b/src/headers/www_authenticate/challenge/bearer.rs new file mode 100644 index 000000000..c3885e1aa --- /dev/null +++ b/src/headers/www_authenticate/challenge/bearer.rs @@ -0,0 +1,136 @@ +use std::str; +use std::fmt; +use std::default::Default; + +use bytes::{BufMut, Bytes, BytesMut}; +use actix_web::http::StatusCode; +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; + +use super::Challenge; + +/// Bearer authorization error types, described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3.1) +#[derive(Debug, Copy, Clone)] +pub enum Error { + /// The request is missing a required parameter, includes an unsupported parameter + /// or parameter value, repeats the same parameter, uses more than one method + /// for including an access token, or is otherwise malformed. + InvalidRequest, + + /// The access token provided is expired, revoked, malformed, or invalid for other reasons. + InvalidToken, + + /// The request requires higher privileges than provided by the access token. + InsufficientScope, +} + +impl Error { + pub fn status_code(&self) -> StatusCode { + match *self { + Error::InvalidRequest => StatusCode::BAD_REQUEST, + Error::InvalidToken => StatusCode::UNAUTHORIZED, + Error::InsufficientScope => StatusCode::FORBIDDEN, + } + } + + fn as_str(&self) -> &'static str { + match *self { + Error::InvalidRequest => "invalid_request", + Error::InvalidToken => "invalid_token", + Error::InsufficientScope => "insufficient_scope", + } + } +} + +/// Challenge for `WWW-Authenticate` header with HTTP Bearer auth scheme, +/// described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3) +#[derive(Debug, Clone)] +pub struct Bearer { + pub scope: Option, + pub realm: Option, + pub error: Option, + pub error_description: Option, + /// It is up to implementor to provide correct absolute URI + pub error_uri: Option, +} + +impl Challenge for Bearer { + fn to_bytes(&self) -> Bytes { + 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) + + 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 mut buffer = BytesMut::with_capacity(capacity); + buffer.put("Bearer"); + + if let Some(ref realm) = self.realm { + buffer.put(" realm=\""); + buffer.put(realm); + buffer.put("\""); + } + + if let Some(ref scope) = self.scope { + buffer.put(" scope=\""); + buffer.put(scope); + buffer.put("\""); + } + + if let Some(ref error) = self.error { + let error_repr = error.as_str(); + let remaining = buffer.remaining_mut(); + let required = error_repr.len() + 9; // 9 is for `" error=\"\""` + if remaining < required { + buffer.reserve(required - remaining); + } + buffer.put(" error=\""); + buffer.put(error_repr); + buffer.put("\"") + } + + if let Some(ref error_description) = self.error_description { + buffer.put(" error_description=\""); + buffer.put(error_description); + buffer.put("\""); + } + + if let Some(ref error_uri) = self.error_uri { + buffer.put(" error_uri=\""); + buffer.put(error_uri); + buffer.put("\""); + } + + buffer.freeze() + } +} + +impl Default for Bearer { + fn default() -> Self { + Bearer { + scope: None, + realm: None, + error: None, + error_description: None, + error_uri: None, + } + } +} + +impl fmt::Display for Bearer { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let bytes = self.to_bytes(); + let repr = str::from_utf8(&bytes) + // Should not happen since challenges are crafted manually + // from `&'static str`'s and Strings + .map_err(|_| fmt::Error)?; + + f.write_str(repr) + } +} + +impl IntoHeaderValue for Bearer { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result::Error> { + HeaderValue::from_shared(self.to_bytes()) + } +} diff --git a/src/headers/www_authenticate/challenge/mod.rs b/src/headers/www_authenticate/challenge/mod.rs new file mode 100644 index 000000000..372de9884 --- /dev/null +++ b/src/headers/www_authenticate/challenge/mod.rs @@ -0,0 +1,12 @@ +use std::fmt::{Debug, Display}; + +use bytes::Bytes; +use actix_web::http::header::IntoHeaderValue; + +pub mod basic; +pub mod bearer; + +/// Authentication challenge for `WWW-Authenticate` header. +pub trait Challenge: IntoHeaderValue + Debug + Display + Clone + Send + Sync { + fn to_bytes(&self) -> Bytes; +} diff --git a/src/headers/www_authenticate/header.rs b/src/headers/www_authenticate/header.rs new file mode 100644 index 000000000..d439ea97b --- /dev/null +++ b/src/headers/www_authenticate/header.rs @@ -0,0 +1,49 @@ +use actix_web::{HttpMessage}; +use actix_web::error::ParseError; +use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE}; + +use super::Challenge; + +/// `WWW-Authenticate` header, described in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.1) +/// +/// `WWW-Authenticate` header is generic over [Challenge](./trait.Challenge.html) +/// +/// # Example +/// +/// ```rust +/// # extern crate actix_web; +/// # extern crate actix_web_httpauth; +/// +/// use actix_web::{HttpRequest, HttpResponse}; +/// use actix_web::http::StatusCode; +/// use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate}; +/// use actix_web_httpauth::headers::www_authenticate::basic::Basic; +/// +/// fn handler(req: HttpRequest) -> HttpResponse { +/// let challenge = Basic { +/// realm: Some("Restricted area".to_string()), +/// }; +/// req.build_response(StatusCode::UNAUTHORIZED) +/// .set(WWWAuthenticate(challenge)) +/// .finish() +/// } +/// ``` +pub struct WWWAuthenticate(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/src/headers/www_authenticate/mod.rs b/src/headers/www_authenticate/mod.rs new file mode 100644 index 000000000..3c874a48a --- /dev/null +++ b/src/headers/www_authenticate/mod.rs @@ -0,0 +1,7 @@ +mod challenge; +mod header; + +pub use self::header::WWWAuthenticate; +pub use self::challenge::Challenge; +pub use self::challenge::basic; +pub use self::challenge::bearer; diff --git a/src/lib.rs b/src/lib.rs index bcd182dcd..f97de92c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,15 @@ -//! HTTP authorization routines for [actix-web](https://github.com/actix/actix-web) framework. +//! HTTP Authorization support for [actix-web](https://actix.rs) framework. //! -//! Currently supported schemas: -//! * Basic ([RFC-7617](https://tools.ietf.org/html/rfc7617)) +//! Provides [`Authorization`](./headers/authorization/struct.Authorization.html) +//! and [`WWW-Authenticate`](./headers/www_authenticate/struct.WWWAuthenticate.html) headers, +//! and `actix-web` extractors for an `Authorization` header. + +#![cfg_attr(feature = "nightly", feature(test))] +#[cfg(feature = "nightly")] extern crate test; -extern crate bytes; -extern crate percent_encoding; extern crate actix_web; +extern crate bytes; extern crate base64; -mod errors; -pub mod basic; +pub mod headers; +pub mod extractors; From 488b7bf6535dd3c8668e66a10431d6c2cad6ce6d Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Mon, 18 Jun 2018 09:21:20 +0200 Subject: [PATCH 10/36] Add test and fix for panic in Bearer::to_bytes (#1) --- .../www_authenticate/challenge/bearer.rs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/headers/www_authenticate/challenge/bearer.rs b/src/headers/www_authenticate/challenge/bearer.rs index c3885e1aa..04d75f4e8 100644 --- a/src/headers/www_authenticate/challenge/bearer.rs +++ b/src/headers/www_authenticate/challenge/bearer.rs @@ -55,11 +55,13 @@ pub struct Bearer { 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) + - self.error_description.as_ref().map_or(0, |desc| desc.len() + 20) + - self.error_uri.as_ref().map_or(0, |url| url.len() + 12); + desc_uri_required; let mut buffer = BytesMut::with_capacity(capacity); buffer.put("Bearer"); @@ -78,9 +80,9 @@ impl Challenge for Bearer { if let Some(ref error) = self.error { let error_repr = error.as_str(); let remaining = buffer.remaining_mut(); - let required = error_repr.len() + 9; // 9 is for `" error=\"\""` + let required = desc_uri_required + error_repr.len() + 9; // 9 is for `" error=\"\""` if remaining < required { - buffer.reserve(required - remaining); + buffer.reserve(required); } buffer.put(" error=\""); buffer.put(error_repr); @@ -134,3 +136,21 @@ impl IntoHeaderValue for Bearer { HeaderValue::from_shared(self.to_bytes()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn to_bytes() { + let b = Bearer { + scope: None, + realm: None, + error: Some(Error::InvalidToken), + error_description: Some(String::from("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found")), + error_uri: None, + }; + assert_eq!("Bearer error=\"invalid_token\" error_description=\"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found\"", + format!("{}", b)); + } +} From 67f0aceea6f08c98a7aed321d92bb176aee57f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20H=C3=B6ppner?= Date: Sun, 1 Jul 2018 15:25:00 +0200 Subject: [PATCH 11/36] Account for joining ':' in basic auth (#2) --- src/headers/authorization/scheme/basic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 449ea2ad6..1af2868cf 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -70,7 +70,7 @@ impl IntoHeaderValue for Basic { fn try_into(self) -> Result::Error> { let mut credentials = BytesMut::with_capacity( - self.username.len() + self.password.as_ref().map_or(0, |pwd| pwd.len()) + self.username.len() + 1 + self.password.as_ref().map_or(0, |pwd| pwd.len()) ); credentials.put(&self.username); credentials.put_u8(b':'); From 0495d783846ae3fbffbf111e2adc960550403256 Mon Sep 17 00:00:00 2001 From: svartalf Date: Sun, 1 Jul 2018 16:30:58 +0300 Subject: [PATCH 12/36] Bumping version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3bb8ce0f3..1fbfbea7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.0.3" +version = "0.0.4" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" From f79a253f1ecae7dbca4adde1f33b9edaf4e4f1b9 Mon Sep 17 00:00:00 2001 From: svartalf Date: Sun, 1 Jul 2018 16:52:10 +0300 Subject: [PATCH 13/36] Create CHANGELOG.md --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..0c456ec64 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# 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.0.4] - 2017-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 From 411d08c121136e30d4bf3cf48364b1dce32f7742 Mon Sep 17 00:00:00 2001 From: Masaki Hara Date: Sat, 8 Sep 2018 16:27:21 +0900 Subject: [PATCH 14/36] Update actix-web to 0.7. (Closes #4) --- Cargo.toml | 2 +- examples/extractor_basic.rs | 2 +- examples/extractor_bearer.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1fbfbea7f..bdb1bce89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ license = "MIT/Apache-2.0" exclude = [".travis.yml", ".gitignore"] [dependencies] -actix-web = "0.6" +actix-web = "0.7" bytes = "0.4" base64 = "0.9" diff --git a/examples/extractor_basic.rs b/examples/extractor_basic.rs index 8e63ae397..043289a04 100644 --- a/examples/extractor_basic.rs +++ b/examples/extractor_basic.rs @@ -9,7 +9,7 @@ use actix_web_httpauth::extractors::AuthenticationError; struct Auth; impl Middleware for Auth { - fn start(&self, req: &mut HttpRequest) -> Result { + fn start(&self, req: &HttpRequest) -> Result { let mut config = Config::default(); config.realm("WallyWorld"); let auth = BasicAuth::from_request(&req, &config)?; diff --git a/examples/extractor_bearer.rs b/examples/extractor_bearer.rs index 145abecca..560eb74d4 100644 --- a/examples/extractor_bearer.rs +++ b/examples/extractor_bearer.rs @@ -9,7 +9,7 @@ use actix_web::middleware::{Middleware, Started}; struct Auth; impl Middleware for Auth { - fn start(&self, req: &mut HttpRequest) -> Result { + fn start(&self, req: &HttpRequest) -> Result { let mut config = Config::default(); config.realm("Restricted area"); config.scope("openid profile email"); From fe288f542299b3d4c73ee10bfff15b8c00f05ec3 Mon Sep 17 00:00:00 2001 From: svartalf Date: Sat, 8 Sep 2018 10:40:37 +0300 Subject: [PATCH 15/36] Bumping version since actix-web dependency version had changed --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bdb1bce89..27e192ba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.0.4" +version = "0.1.0" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" From 51035f78c883efda7f2a64d94d97d4904e32efa7 Mon Sep 17 00:00:00 2001 From: svartalf Date: Sat, 8 Sep 2018 10:44:39 +0300 Subject: [PATCH 16/36] Update CHANGELOG.md --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c456ec64..b4f5f77d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ 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.0.4] - 2017-07-01 +## [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 From 2de26a5906915f23f1d4b1b8b415d32cdad9141d Mon Sep 17 00:00:00 2001 From: Justin Kilpatrick Date: Fri, 26 Apr 2019 14:42:33 -0400 Subject: [PATCH 17/36] Use actix-web without default features (#6) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 27e192ba0..95a7a68cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ license = "MIT/Apache-2.0" exclude = [".travis.yml", ".gitignore"] [dependencies] -actix-web = "0.7" +actix-web = { version = "0.7", default_features = false } bytes = "0.4" base64 = "0.9" From cb158fbe8daf55396c9528c13319eeddefa9ab09 Mon Sep 17 00:00:00 2001 From: svartalf Date: Fri, 26 Apr 2019 21:56:17 +0300 Subject: [PATCH 18/36] Bumping version to 0.2.0 --- CHANGELOG.md | 5 +++++ Cargo.toml | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4f5f77d3..734c338be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ 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.2.0] - 2019-04-26 +### Changed + - `actix-web` dependency is used without default features now ([#6](https://github.com/svartalf/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 diff --git a/Cargo.toml b/Cargo.toml index 95a7a68cb..d0a388dff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.1.0" +version = "0.2.0" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -15,7 +15,7 @@ exclude = [".travis.yml", ".gitignore"] [dependencies] actix-web = { version = "0.7", default_features = false } bytes = "0.4" -base64 = "0.9" +base64 = "0.10" [features] default = [] @@ -23,3 +23,4 @@ nightly = [] [badges] travis-ci = { repository = "svartalf/actix-web-httpauth", branch = "master" } +maintenance = { status = "passively-maintained" } From 7a9b3924cb205d665d889385305d5aff4f8ad1a3 Mon Sep 17 00:00:00 2001 From: svartalf Date: Wed, 15 May 2019 17:38:55 +0300 Subject: [PATCH 19/36] Upgrage to the actix-web 1.0 -- alpha.1 version --- .editorconfig | 9 + .travis.yml | 3 - CHANGELOG.md | 6 + Cargo.toml | 11 +- examples/extractor_basic.rs | 36 ---- examples/extractor_bearer.rs | 40 ----- examples/header_www_authenticate_basic.rs | 25 --- examples/header_www_authenticate_bearer.rs | 29 ---- examples/middleware_basic.rs | 89 ++++++++++ rustfmt.toml | 9 + src/extractors/basic.rs | 124 ++++++++++---- src/extractors/bearer.rs | 118 ++++++++++--- src/extractors/config.rs | 8 +- src/extractors/errors.rs | 35 ++-- src/extractors/mod.rs | 6 +- src/headers/authorization/errors.rs | 12 +- src/headers/authorization/header.rs | 70 ++++---- src/headers/authorization/mod.rs | 14 +- src/headers/authorization/scheme/basic.rs | 104 ++++++++---- src/headers/authorization/scheme/bearer.rs | 46 ++++-- src/headers/authorization/scheme/mod.rs | 7 +- src/headers/mod.rs | 2 + .../www_authenticate/challenge/basic.rs | 85 ++++++++-- .../www_authenticate/challenge/bearer.rs | 156 ------------------ .../challenge/bearer/builder.rs | 63 +++++++ .../challenge/bearer/challenge.rs | 132 +++++++++++++++ .../challenge/bearer/errors.rs | 48 ++++++ .../www_authenticate/challenge/bearer/mod.rs | 12 ++ .../challenge/bearer/tests.rs | 14 ++ src/headers/www_authenticate/challenge/mod.rs | 5 +- src/headers/www_authenticate/header.rs | 34 +--- src/headers/www_authenticate/mod.rs | 6 +- src/lib.rs | 24 ++- src/utils.rs | 13 ++ 34 files changed, 874 insertions(+), 521 deletions(-) create mode 100644 .editorconfig delete mode 100644 examples/extractor_basic.rs delete mode 100644 examples/extractor_bearer.rs delete mode 100644 examples/header_www_authenticate_basic.rs delete mode 100644 examples/header_www_authenticate_bearer.rs create mode 100644 examples/middleware_basic.rs create mode 100644 rustfmt.toml delete mode 100644 src/headers/www_authenticate/challenge/bearer.rs create mode 100644 src/headers/www_authenticate/challenge/bearer/builder.rs create mode 100644 src/headers/www_authenticate/challenge/bearer/challenge.rs create mode 100644 src/headers/www_authenticate/challenge/bearer/errors.rs create mode 100644 src/headers/www_authenticate/challenge/bearer/mod.rs create mode 100644 src/headers/www_authenticate/challenge/bearer/tests.rs create mode 100644 src/utils.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..f6da06d69 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/.travis.yml b/.travis.yml index 45793de3b..d2da893a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,3 @@ rust: - stable - beta - nightly -matrix: - allow_failures: - - rust: nightly diff --git a/CHANGELOG.md b/CHANGELOG.md index 734c338be..82ff6c8cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## Unreleased + - Crate edition was changed to `2018`, same as `actix-web` + - Depends on `actix-web = "^1.0"` version now + - `WWWAuthenticate` header struct was renamed into `WwwAuthenticate` + - Challenges and extractor configs are now operating with `Cow<'static, str>` types instead of `String` types + ## [0.2.0] - 2019-04-26 ### Changed - `actix-web` dependency is used without default features now ([#6](https://github.com/svartalf/actix-web-httpauth/pull/6)) diff --git a/Cargo.toml b/Cargo.toml index d0a388dff..782e5cb38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.2.0" +version = "0.3.0-alpha.1" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -9,14 +9,19 @@ homepage = "https://github.com/svartalf/actix-web-httpauth" repository = "https://github.com/svartalf/actix-web-httpauth.git" documentation = "https://docs.rs/actix-web-httpauth/" categories = ["web-programming::http-server"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" exclude = [".travis.yml", ".gitignore"] +edition = "2018" [dependencies] -actix-web = { version = "0.7", default_features = false } +actix-web = { version = "1.0.0-beta.5", default_features = false } bytes = "0.4" base64 = "0.10" +[dev-dependencies] +futures = "0.1.27" +actix-service = "0.4.0" + [features] default = [] nightly = [] diff --git a/examples/extractor_basic.rs b/examples/extractor_basic.rs deleted file mode 100644 index 043289a04..000000000 --- a/examples/extractor_basic.rs +++ /dev/null @@ -1,36 +0,0 @@ -extern crate actix_web; -extern crate actix_web_httpauth; - -use actix_web::{server, App, Result, HttpRequest, FromRequest}; -use actix_web::middleware::{Middleware, Started}; -use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; -use actix_web_httpauth::extractors::AuthenticationError; - -struct Auth; - -impl Middleware for Auth { - fn start(&self, req: &HttpRequest) -> Result { - let mut config = Config::default(); - config.realm("WallyWorld"); - let auth = BasicAuth::from_request(&req, &config)?; - - if auth.username() == "Aladdin" && auth.password() == Some("open sesame") { - Ok(Started::Done) - } else { - Err(AuthenticationError::from(config).into()) - } - } -} - -fn index(_req: HttpRequest) -> String { - "Hello, authorized user!".to_string() -} - -fn main() { - server::new(|| App::new() - .middleware(Auth) - .resource("/", |r| r.with(index)) - ) - .bind("127.0.0.1:8088").unwrap() - .run(); -} diff --git a/examples/extractor_bearer.rs b/examples/extractor_bearer.rs deleted file mode 100644 index 560eb74d4..000000000 --- a/examples/extractor_bearer.rs +++ /dev/null @@ -1,40 +0,0 @@ -extern crate actix_web; -extern crate actix_web_httpauth; - -use actix_web::{server, App, HttpRequest, Result, FromRequest}; -use actix_web_httpauth::extractors::AuthenticationError; -use actix_web_httpauth::extractors::bearer::{BearerAuth, Config, Error}; -use actix_web::middleware::{Middleware, Started}; - -struct Auth; - -impl Middleware for Auth { - fn start(&self, req: &HttpRequest) -> Result { - let mut config = Config::default(); - config.realm("Restricted area"); - config.scope("openid profile email"); - let auth = BearerAuth::from_request(&req, &config)?; - - if auth.token() == "mF_9.B5f-4.1JqM" { - Ok(Started::Done) - } else { - Err(AuthenticationError::from(config) - .with_error(Error::InvalidToken) - .into()) - } - } - -} - -fn index(_req: HttpRequest) -> String { - "Hello, authorized user!".to_string() -} - -fn main() { - server::new(|| App::new() - .middleware(Auth) - .resource("/", |r| r.with(index)) - ) - .bind("127.0.0.1:8088").unwrap() - .run(); -} diff --git a/examples/header_www_authenticate_basic.rs b/examples/header_www_authenticate_basic.rs deleted file mode 100644 index 031948b20..000000000 --- a/examples/header_www_authenticate_basic.rs +++ /dev/null @@ -1,25 +0,0 @@ -extern crate actix_web; -extern crate actix_web_httpauth; - -use actix_web::{server, App, HttpRequest, HttpResponse}; -use actix_web::http::StatusCode; -use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate}; -use actix_web_httpauth::headers::www_authenticate::basic::Basic; - - -fn index(req: HttpRequest) -> HttpResponse { - let challenge = Basic { - realm: Some("Restricted area".to_string()), - }; - - req.build_response(StatusCode::UNAUTHORIZED) - .set(WWWAuthenticate(challenge)) - .finish() -} - -fn main() { - server::new(|| App::new() - .resource("/", |r| r.with(index))) - .bind("127.0.0.1:8088").unwrap() - .run(); -} diff --git a/examples/header_www_authenticate_bearer.rs b/examples/header_www_authenticate_bearer.rs deleted file mode 100644 index 65d24b822..000000000 --- a/examples/header_www_authenticate_bearer.rs +++ /dev/null @@ -1,29 +0,0 @@ -extern crate actix_web; -extern crate actix_web_httpauth; - -use actix_web::{server, App, HttpRequest, HttpResponse}; -use actix_web::http::StatusCode; -use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate}; -use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer, Error}; - - -fn index(req: HttpRequest) -> HttpResponse { - let challenge = Bearer { - realm: Some("example".to_string()), - scope: Some("openid profile email".to_string()), - error: Some(Error::InvalidToken), - error_description: Some("The access token expired".to_string()), - error_uri: Some("http://example.org".to_string()), - }; - - req.build_response(StatusCode::UNAUTHORIZED) - .set(WWWAuthenticate(challenge)) - .finish() -} - -fn main() { - server::new(|| App::new() - .resource("/", |r| r.with(index))) - .bind("127.0.0.1:8088").unwrap() - .run(); -} diff --git a/examples/middleware_basic.rs b/examples/middleware_basic.rs new file mode 100644 index 000000000..a71859ed5 --- /dev/null +++ b/examples/middleware_basic.rs @@ -0,0 +1,89 @@ +use std::borrow::Cow; +use std::io; + +use actix_service::{Service, Transform}; +use actix_web::{dev, web, App, Error, HttpRequest, HttpServer}; +use futures::future::{self, Either, FutureResult}; +use futures::Poll; + +use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; +use actix_web_httpauth::extractors::AuthenticationError; + +struct Auth(Config); + +impl Transform for Auth +where + S: Service, Error = Error>, + S::Future: 'static, +{ + type Request = dev::ServiceRequest; + type Response = dev::ServiceResponse; + type Error = Error; + type Transform = AuthMiddleware; + type InitError = (); + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + future::ok(AuthMiddleware { + service, + auth: self.0.clone(), + }) + } +} + +struct AuthMiddleware { + service: S, + auth: Config, +} + +impl AuthMiddleware { + fn valid_user(credentials: &BasicAuth) -> bool { + let user_id = credentials.user_id(); + let password = credentials.password(); + + user_id == "Alladin" && password == Some(&Cow::Borrowed("open sesame")) + } +} + +impl Service for AuthMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, +{ + type Request = dev::ServiceRequest; + type Response = dev::ServiceResponse; + type Error = Error; + type Future = Either>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready() + } + + fn call(&mut self, mut req: dev::ServiceRequest) -> Self::Future { + let auth = BasicAuth::from_service_request(&mut req, &self.auth); + + match auth { + Ok(ref credentials) if Self::valid_user(credentials) => Either::A(self.service.call(req)), + Ok(..) => { + let challenge = self.auth.as_ref().clone(); + let error = AuthenticationError::new(challenge); + Either::B(future::err(Self::Error::from(error))) + } + Err(e) => Either::B(future::err(e.into())), + } + } +} + +fn index(_req: HttpRequest) -> String { + "Hello, authorized user!".to_string() +} + +fn main() -> io::Result<()> { + HttpServer::new(|| { + let config = Config::default().realm("WallyWorld"); + + App::new().wrap(Auth(config)).service(web::resource("/").to(index)) + }) + .bind("127.0.0.1:8088")? + .run() +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..599ac809c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,9 @@ +unstable_features = true +edition = "2018" +version = "Two" +wrap_comments = true +comment_width = 120 +max_width = 120 +merge_imports = false +newline_style = "Unix" +struct_lit_single_line = false diff --git a/src/extractors/basic.rs b/src/extractors/basic.rs index 484058bb6..98f62e398 100644 --- a/src/extractors/basic.rs +++ b/src/extractors/basic.rs @@ -1,16 +1,22 @@ -use std::default::Default; +//! Extractor for the "Basic" HTTP Authentication Scheme -use actix_web::{HttpRequest, FromRequest}; +use std::borrow::Cow; + +use actix_web::dev::{Payload, ServiceRequest}; use actix_web::http::header::Header; +use actix_web::{FromRequest, HttpRequest}; -use headers::authorization::{Authorization, Basic}; -use headers::www_authenticate::basic::Basic as Challenge; -use super::errors::AuthenticationError; use super::config::ExtractorConfig; +use super::errors::AuthenticationError; +use crate::headers::authorization::{Authorization, Basic}; +use crate::headers::www_authenticate::basic::Basic as Challenge; -/// [`BasicAuth`](./struct.BasicAuth.html) extractor configuration, -/// used for `WWW-Authenticate` header later. -#[derive(Debug, Clone)] +/// [`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 { @@ -18,12 +24,21 @@ impl Config { /// /// 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) -> &mut Config { + 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 ExtractorConfig for Config { type Inner = Challenge; @@ -32,49 +47,94 @@ impl ExtractorConfig for Config { } } -impl Default for Config { - fn default() -> Self { - Config(Challenge::default()) - } -} - -/// Extractor for HTTP Basic auth +/// Extractor for HTTP Basic auth. /// /// # Example /// /// ```rust -/// # extern crate actix_web; -/// # extern crate actix_web_httpauth; /// use actix_web::Result; /// use actix_web_httpauth::extractors::basic::BasicAuth; /// -/// fn index(auth: BasicAuth) -> Result { -/// Ok(format!("Hello, {}!", auth.username())) +/// 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 +/// +/// ```rust +/// use actix_web::{web, App}; +/// use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; +/// +/// fn index(auth: BasicAuth) -> String { +/// format!("Hello, {}!", auth.user_id()) +/// } +/// +/// fn main() { +/// let app = App::new() +/// .data(Config::default().realm("Restricted area")) +/// .service(web::resource("/index.html").route(web::get().to(index))); +/// } +/// ``` +/// +/// [`Config`]: ./struct.Config.html +/// [app data]: https://docs.rs/actix-web/1.0.0-beta.5/actix_web/struct.App.html#method.data #[derive(Debug, Clone)] pub struct BasicAuth(Basic); impl BasicAuth { - pub fn username(&self) -> &str { - self.0.username.as_str() + /// Returns client's user-ID. + pub fn user_id(&self) -> &Cow<'static, str> { + &self.0.user_id() } - pub fn password(&self) -> Option<&str> { - match self.0.password { - None => None, - Some(ref pwd) => Some(pwd.as_str()) - } + /// Returns client's password. + pub fn password(&self) -> Option<&Cow<'static, str>> { + self.0.password() + } + + /// Try to parse actix-web' `ServiceRequest` and fetch the `BasicAuth` from it. + /// + /// ## Warning + /// + /// This function is used right now for middleware creation only + /// and might change or be totally removed, + /// depends on `actix-web = "1.0"` release changes. + /// + /// This issue will be resolved in the `0.3.0` release. + /// Before that -- brace yourselves! + pub fn from_service_request(req: &mut ServiceRequest, config: &Config) -> ::Future { + Authorization::::parse(&req) + .map(|auth| BasicAuth(auth.into_scheme())) + .map_err(|_| { + // TODO: debug! the original error + let challenge = config.clone().into_inner(); + + AuthenticationError::new(challenge) + }) } } -impl FromRequest for BasicAuth { +impl FromRequest for BasicAuth { + type Future = Result; type Config = Config; - type Result = Result>; + type Error = AuthenticationError; - fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { + fn from_request(req: &HttpRequest, _: &mut Payload) -> ::Future { Authorization::::parse(req) - .map(|auth| BasicAuth(auth.into_inner())) - .map_err(|_| AuthenticationError::new(cfg.0.clone())) + .map(|auth| BasicAuth(auth.into_scheme())) + .map_err(|_| { + // TODO: debug! the original error + let challenge = req + .get_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/src/extractors/bearer.rs b/src/extractors/bearer.rs index b3c9a5182..0b88e63a0 100644 --- a/src/extractors/bearer.rs +++ b/src/extractors/bearer.rs @@ -1,16 +1,20 @@ +//! Extractor for the "Bearer" HTTP Authentication Scheme + +use std::borrow::Cow; use std::default::Default; -use actix_web::{HttpRequest, FromRequest}; +use actix_web::dev::{Payload, ServiceRequest}; use actix_web::http::header::Header; +use actix_web::{FromRequest, HttpRequest}; -use headers::authorization; -use headers::www_authenticate::bearer; -pub use headers::www_authenticate::bearer::Error; -use super::errors::AuthenticationError; use super::config::ExtractorConfig; +use super::errors::AuthenticationError; +use crate::headers::authorization; +use crate::headers::www_authenticate::bearer; +pub use crate::headers::www_authenticate::bearer::Error; /// [BearerAuth](./struct/BearerAuth.html) extractor configuration. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Config(bearer::Bearer); impl Config { @@ -18,7 +22,7 @@ impl Config { /// /// 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) -> &mut Config { + pub fn scope>>(mut self, value: T) -> Config { self.0.scope = Some(value.into()); self } @@ -27,12 +31,18 @@ impl Config { /// /// 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) -> &mut Config { + 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 ExtractorConfig for Config { type Inner = bearer::Bearer; @@ -41,60 +51,114 @@ impl ExtractorConfig for Config { } } -impl Default for Config { - fn default() -> Self { - Config(bearer::Bearer::default()) - } -} - /// Extractor for HTTP Bearer auth /// /// # Example /// /// ```rust -/// # extern crate actix_web; -/// # extern crate actix_web_httpauth; -/// use actix_web::Result; /// use actix_web_httpauth::extractors::bearer::BearerAuth; /// -/// fn index(auth: BearerAuth) -> Result { -/// Ok(format!("Hello, user with token {}!", auth.token())) +/// 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 +/// +/// ```rust +/// use actix_web::{web, App}; +/// use actix_web_httpauth::extractors::bearer::{BearerAuth, Config}; +/// +/// fn index(auth: BearerAuth) -> String { +/// format!("Hello, {}!", auth.token()) +/// } +/// +/// fn main() { +/// let app = App::new() +/// .data(Config::default().realm("Restricted area").scope("email photo")) +/// .service(web::resource("/index.html").route(web::get().to(index))); /// } /// ``` #[derive(Debug, Clone)] pub struct BearerAuth(authorization::Bearer); impl BearerAuth { + /// Returns bearer token provided by client. pub fn token(&self) -> &str { - self.0.token.as_str() + self.0.token() + } + + /// Try to parse actix-web' `ServiceRequest` and fetch the `BasicAuth` from it. + /// + /// ## Warning + /// + /// This function is used right now for middleware creation only + /// and might change or be totally removed, + /// depends on `actix-web = "1.0"` release changes. + /// + /// This issue will be resolved in the `0.3.0` release. + /// Before that -- brace yourselves! + pub fn from_service_request(req: &mut ServiceRequest, config: &Config) -> ::Future { + authorization::Authorization::::parse(&req) + .map(|auth| BearerAuth(auth.into_scheme())) + .map_err(|_| { + // TODO: debug! the original error + let challenge = config.clone().into_inner(); + + AuthenticationError::new(challenge) + }) } } -impl FromRequest for BearerAuth { +impl FromRequest for BearerAuth { type Config = Config; - type Result = Result>; + type Future = Result; + type Error = AuthenticationError; - fn from_request(req: &HttpRequest, cfg: &>::Config) -> >::Result { + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> ::Future { authorization::Authorization::::parse(req) - .map(|auth| BearerAuth(auth.into_inner())) - .map_err(|_| AuthenticationError::new(cfg.0.clone())) + .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 } - pub fn with_error_description>(mut self, desc: T) -> 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 } - pub fn with_error_uri>(mut self, uri: T) -> Self { + /// Attach error URI to the current Authentication error. + /// + /// It is up to implementor to provide properly formed absolute URI. + pub fn with_error_uri(mut self, uri: T) -> Self + where + T: Into>, + { self.challenge_mut().error_uri = Some(uri.into()); self } diff --git a/src/extractors/config.rs b/src/extractors/config.rs index 9337bffa5..721cf8f5b 100644 --- a/src/extractors/config.rs +++ b/src/extractors/config.rs @@ -1,6 +1,5 @@ -use headers::www_authenticate::Challenge; - use super::AuthenticationError; +use crate::headers::www_authenticate::Challenge; pub trait ExtractorConfig { type Inner: Challenge; @@ -8,7 +7,10 @@ pub trait ExtractorConfig { fn into_inner(self) -> Self::Inner; } -impl From for AuthenticationError<::Inner> where T: ExtractorConfig { +impl From for AuthenticationError<::Inner> +where + T: ExtractorConfig, +{ fn from(config: T) -> Self { AuthenticationError::new(config.into_inner()) } diff --git a/src/extractors/errors.rs b/src/extractors/errors.rs index 3d161333b..1834f6b8a 100644 --- a/src/extractors/errors.rs +++ b/src/extractors/errors.rs @@ -1,14 +1,13 @@ -use std::str; -use std::fmt; use std::error::Error; +use std::fmt; -use actix_web::{HttpResponse, ResponseError}; use actix_web::http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; -use headers::www_authenticate::{WWWAuthenticate}; -use headers::www_authenticate::Challenge; +use crate::headers::www_authenticate::Challenge; +use crate::headers::www_authenticate::WwwAuthenticate; -/// Authentication error returned by Auth extractor. +/// Authentication error returned by authentication extractors. /// /// Different extractors may extend `AuthenticationError` implementation /// in order to provide access to inner challenge fields. @@ -19,6 +18,9 @@ pub struct AuthenticationError { } 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, @@ -26,10 +28,15 @@ impl AuthenticationError { } } + /// 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 } @@ -37,27 +44,17 @@ impl AuthenticationError { impl fmt::Display for AuthenticationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let bytes = self.challenge.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) + fmt::Display::fmt(&self.status_code, f) } } -impl Error for AuthenticationError { - fn description(&self) -> &str { - unimplemented!() - } -} +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())) + .set(WwwAuthenticate(self.challenge.clone())) .finish() } } diff --git a/src/extractors/mod.rs b/src/extractors/mod.rs index be364ab26..797c99713 100644 --- a/src/extractors/mod.rs +++ b/src/extractors/mod.rs @@ -1,6 +1,8 @@ -mod errors; -mod config; +//! Type-safe authentication information extractors + pub mod basic; pub mod bearer; +mod config; +mod errors; pub use self::errors::AuthenticationError; diff --git a/src/headers/authorization/errors.rs b/src/headers/authorization/errors.rs index abdb80f9b..6ba240507 100644 --- a/src/headers/authorization/errors.rs +++ b/src/headers/authorization/errors.rs @@ -1,9 +1,8 @@ -use std::str; -use std::fmt; -use std::error::Error; use std::convert::From; +use std::error::Error; +use std::fmt; +use std::str; -use base64; use actix_web::http::header; /// Possible errors while parsing `Authorization` header. @@ -18,8 +17,11 @@ pub enum ParseError { 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), } @@ -41,7 +43,7 @@ impl Error for ParseError { } } - fn cause(&self) -> Option<&Error> { + fn source(&self) -> Option<&(dyn Error + 'static)> { match self { ParseError::Invalid => None, ParseError::MissingScheme => None, diff --git a/src/headers/authorization/header.rs b/src/headers/authorization/header.rs index ab801f3bf..740af59b5 100644 --- a/src/headers/authorization/header.rs +++ b/src/headers/authorization/header.rs @@ -1,12 +1,10 @@ -use std::ops; use std::fmt; -use actix_web::{HttpMessage}; use actix_web::error::ParseError; use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION}; +use actix_web::HttpMessage; -use headers::authorization::scheme::Scheme; - +use crate::headers::authorization::scheme::Scheme; /// `Authorization` header, defined in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.2) /// @@ -21,27 +19,57 @@ use headers::authorization::scheme::Scheme; /// # Example /// /// ```rust -/// # extern crate actix_web; -/// # extern crate actix_web_httpauth; -/// -/// use actix_web::{HttpRequest, Result}; -/// use actix_web::http::header::Header; -/// use actix_web_httpauth::headers::authorization::{Authorization, Basic}; -/// +/// # 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.username)) +/// Ok(format!("Hello, {}!", auth.as_ref().user_id())) /// } /// ``` +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)] pub struct Authorization(S); -impl Authorization { - pub fn into_inner(self) -> 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 { @@ -69,17 +97,3 @@ impl fmt::Display for Authorization { fmt::Display::fmt(&self.0, f) } } - -impl ops::Deref for Authorization { - type Target = S; - - fn deref(&self) -> &::Target { - &self.0 - } -} - -impl ops::DerefMut for Authorization { - fn deref_mut(&mut self) -> &mut ::Target { - &mut self.0 - } -} diff --git a/src/headers/authorization/mod.rs b/src/headers/authorization/mod.rs index d1420a5dc..b02e0531d 100644 --- a/src/headers/authorization/mod.rs +++ b/src/headers/authorization/mod.rs @@ -1,9 +1,11 @@ -mod scheme; -mod header; -mod errors; +//! `Authorization` header and various auth schemes + +mod errors; +mod header; +mod scheme; -pub use self::scheme::Scheme; -pub use self::scheme::basic::Basic; -pub use self::scheme::bearer::Bearer; pub use self::errors::ParseError; pub use self::header::Authorization; +pub use self::scheme::basic::Basic; +pub use self::scheme::bearer::Bearer; +pub use self::scheme::Scheme; diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 1af2868cf..06e8aabc2 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -1,18 +1,51 @@ -use std::str; +use std::borrow::Cow; use std::fmt; +use std::str; +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; use base64; use bytes::{BufMut, BytesMut}; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; -use headers::authorization::Scheme; -use headers::authorization::errors::ParseError; +use crate::headers::authorization::errors::ParseError; +use crate::headers::authorization::Scheme; +use crate::utils; /// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617) #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] pub struct Basic { - pub username: String, - pub password: Option, + user_id: Cow<'static, str>, + password: Option>, +} + +impl Basic { + /// Creates `Basic` credentials with provided `user_id` and optional `password`. + /// + /// ## Example + /// + /// ```rust + /// # use actix_web_httpauth::headers::authorization::Basic; + /// let credentials = Basic::new("Alladin", Some("open sesame")); + /// ``` + pub fn new(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 { @@ -29,24 +62,25 @@ impl Scheme for Basic { } let decoded = base64::decode(parts.next().ok_or(ParseError::Invalid)?)?; - let mut credentials = str::from_utf8(&decoded)? - .splitn(2, ':'); + let mut credentials = str::from_utf8(&decoded)?.splitn(2, ':'); - let username = credentials.next() - .ok_or(ParseError::MissingField("username")) - .map(|username| username.to_string())?; - let password = credentials.next() + 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()) + Some(password.to_string().into()) } })?; - Ok(Basic{ - username, + Ok(Basic { + user_id, password, }) } @@ -54,14 +88,13 @@ impl Scheme for Basic { impl fmt::Debug for Basic { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_fmt(format_args!("Basic {}:******", self.username)) + f.write_fmt(format_args!("Basic {}:******", self.user_id)) } } impl fmt::Display for Basic { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // TODO: Display password also - f.write_fmt(format_args!("Basic {}:******", self.username)) + f.write_fmt(format_args!("Basic {}:******", self.user_id)) } } @@ -69,13 +102,12 @@ impl IntoHeaderValue for Basic { type Error = InvalidHeaderValueBytes; fn try_into(self) -> Result::Error> { - let mut credentials = BytesMut::with_capacity( - self.username.len() + 1 + self.password.as_ref().map_or(0, |pwd| pwd.len()) - ); - credentials.put(&self.username); + let mut credentials = + BytesMut::with_capacity(self.user_id.len() + 1 + self.password.as_ref().map_or(0, |pwd| pwd.len())); + utils::put_cow(&mut credentials, &self.user_id); credentials.put_u8(b':'); if let Some(ref password) = self.password { - credentials.put(password); + utils::put_cow(&mut credentials, password); } // TODO: It would be nice not to allocate new `String` here but write directly to `value` @@ -90,8 +122,8 @@ impl IntoHeaderValue for Basic { #[cfg(test)] mod tests { + use super::{Basic, Scheme}; use actix_web::http::header::{HeaderValue, IntoHeaderValue}; - use super::{Scheme, Basic}; #[test] fn test_parse_header() { @@ -100,8 +132,8 @@ mod tests { assert!(scheme.is_ok()); let scheme = scheme.unwrap(); - assert_eq!(scheme.username, "Aladdin"); - assert_eq!(scheme.password, Some("open sesame".to_string())); + assert_eq!(scheme.user_id, "Aladdin"); + assert_eq!(scheme.password, Some("open sesame".into())); } #[test] @@ -111,7 +143,7 @@ mod tests { assert!(scheme.is_ok()); let scheme = scheme.unwrap(); - assert_eq!(scheme.username, "Aladdin"); + assert_eq!(scheme.user_id, "Aladdin"); assert_eq!(scheme.password, None); } @@ -150,17 +182,19 @@ mod tests { #[test] fn test_into_header_value() { let basic = Basic { - username: "Aladdin".to_string(), - password: Some("open sesame".to_string()), + 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==")); + assert_eq!( + result.unwrap(), + HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==") + ); } } - #[cfg(all(test, feature = "nightly"))] mod benches { use test::Bencher; @@ -172,17 +206,15 @@ mod benches { #[bench] fn bench_parsing(b: &mut Bencher) { let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); - b.iter(|| { - Basic::parse(&value) - }); + b.iter(|| Basic::parse(&value)); } #[bench] fn bench_serializing(b: &mut Bencher) { b.iter(|| { let basic = Basic { - username: "Aladdin".to_string(), - password: Some("open sesame".to_string()), + user_id: "Aladdin".into(), + password: Some("open sesame".into()), }; basic.try_into() diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs index 2a7257cd9..c350c1deb 100644 --- a/src/headers/authorization/scheme/bearer.rs +++ b/src/headers/authorization/scheme/bearer.rs @@ -1,17 +1,43 @@ +use std::borrow::Cow; use std::fmt; -use bytes::{BufMut, BytesMut}; use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use bytes::{BufMut, BytesMut}; -use headers::authorization::scheme::Scheme; -use headers::authorization::errors::ParseError; +use crate::headers::authorization::errors::ParseError; +use crate::headers::authorization::scheme::Scheme; +use crate::utils; /// Credentials for `Bearer` authentication scheme, defined in [RFC6750](https://tools.ietf.org/html/rfc6750) /// /// Should be used in combination with [`Authorization`](./struct.Authorization.html) header. #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] pub struct Bearer { - pub token: String, + token: Cow<'static, str>, +} + +impl Bearer { + /// Creates new `Bearer` credentials with the token provided. + /// + /// ## Example + /// + /// ```rust + /// # use actix_web_httpauth::headers::authorization::Bearer; + /// let credentials = Bearer::new("mF_9.B5f-4.1JqM"); + /// ``` + pub fn new(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 { @@ -29,8 +55,8 @@ impl Scheme for Bearer { let token = parts.next().ok_or(ParseError::Invalid)?; - Ok(Bearer{ - token: token.to_string(), + Ok(Bearer { + token: token.to_string().into(), }) } } @@ -53,7 +79,7 @@ impl IntoHeaderValue for Bearer { fn try_into(self) -> Result::Error> { let mut buffer = BytesMut::with_capacity(7 + self.token.len()); buffer.put("Bearer "); - buffer.put(self.token); + utils::put_cow(&mut buffer, &self.token); HeaderValue::from_shared(buffer.freeze()) } @@ -61,8 +87,8 @@ impl IntoHeaderValue for Bearer { #[cfg(test)] mod tests { + use super::{Bearer, Scheme}; use actix_web::http::header::{HeaderValue, IntoHeaderValue}; - use super::{Scheme, Bearer}; #[test] fn test_parse_header() { @@ -100,9 +126,7 @@ mod tests { #[test] fn test_into_header_value() { - let bearer = Bearer { - token: "mF_9.B5f-4.1JqM".to_string(), - }; + let bearer = Bearer::new("mF_9.B5f-4.1JqM"); let result = bearer.try_into(); assert!(result.is_ok()); diff --git a/src/headers/authorization/scheme/mod.rs b/src/headers/authorization/scheme/mod.rs index 42524129e..5832965f3 100644 --- a/src/headers/authorization/scheme/mod.rs +++ b/src/headers/authorization/scheme/mod.rs @@ -1,13 +1,14 @@ use std::fmt::{Debug, Display}; -use actix_web::http::header::{IntoHeaderValue, HeaderValue}; +use actix_web::http::header::{HeaderValue, IntoHeaderValue}; pub mod basic; pub mod bearer; -use headers::authorization::errors::ParseError; +use crate::headers::authorization::errors::ParseError; /// Authentication scheme for [`Authorization`](./struct.Authorization.html) header. -pub trait Scheme: IntoHeaderValue + Debug + Display + Clone + Send + Sync { +pub trait Scheme: IntoHeaderValue + Debug + Display + Clone + Send + Sync { + /// Try to parse the authentication scheme from the `Authorization` header. fn parse(header: &HeaderValue) -> Result; } diff --git a/src/headers/mod.rs b/src/headers/mod.rs index 0d0d12342..5e38e661a 100644 --- a/src/headers/mod.rs +++ b/src/headers/mod.rs @@ -1,2 +1,4 @@ +//! Typed HTTP headers + pub mod authorization; pub mod www_authenticate; diff --git a/src/headers/www_authenticate/challenge/basic.rs b/src/headers/www_authenticate/challenge/basic.rs index 492b2677c..c3c4f22cb 100644 --- a/src/headers/www_authenticate/challenge/basic.rs +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -1,20 +1,78 @@ -use std::str; -use std::fmt; -use std::default::Default; +//! Challenge for the "Basic" HTTP Authentication Scheme + +use std::borrow::Cow; +use std::default::Default; +use std::fmt; +use std::str; -use bytes::{BufMut, Bytes, BytesMut}; use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use bytes::{BufMut, Bytes, BytesMut}; use super::Challenge; +use crate::utils; -/// Challenge for `WWW-Authenticate` header with HTTP Basic auth scheme, +/// Challenge for [`WWW-Authenticate`] header with HTTP Basic auth scheme, /// described in [RFC 7617](https://tools.ietf.org/html/rfc7617) -#[derive(Debug, Clone)] +/// +/// ## Example +/// +/// ```rust +/// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; +/// use actix_web_httpauth::headers::www_authenticate::basic::Basic; +/// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate; +/// +/// fn index(_req: HttpRequest) -> HttpResponse { +/// let challenge = Basic::with_realm("Restricted area"); +/// +/// HttpResponse::Unauthorized().set(WwwAuthenticate(challenge)).finish() +/// } +/// ``` +/// +/// [`WWW-Authenticate`]: ../struct.WwwAuthenticate.html +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)] pub struct Basic { // "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A - pub realm: Option, + pub(crate) realm: Option>, } +impl Basic { + /// Creates new `Basic` challenge with an empty `realm` field. + /// + /// ## Example + /// + /// ```rust + /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; + /// let challenge = Basic::new(); + /// ``` + pub fn new() -> Basic { + Default::default() + } + + /// Creates new `Basic` challenge from the provided `realm` field value. + /// + /// ## Examples + /// + /// ```rust + /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; + /// let challenge = Basic::with_realm("Restricted area"); + /// ``` + /// + /// ```rust + /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; + /// let my_realm = "Earth realm".to_string(); + /// let challenge = Basic::with_realm(my_realm); + /// ``` + pub fn with_realm(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=\"\""` @@ -23,7 +81,7 @@ impl Challenge for Basic { buffer.put("Basic"); if let Some(ref realm) = self.realm { buffer.put(" realm=\""); - buffer.put(realm); + utils::put_cow(&mut buffer, realm); buffer.put("\""); } @@ -51,15 +109,6 @@ impl IntoHeaderValue for Basic { } } - -impl Default for Basic { - fn default() -> Self { - Self { - realm: None, - } - } -} - #[cfg(test)] mod tests { use super::Basic; @@ -80,7 +129,7 @@ mod tests { #[test] fn test_with_realm_into_header_value() { let challenge = Basic { - realm: Some("Restricted area".to_string()), + realm: Some("Restricted area".into()), }; let value = challenge.try_into(); diff --git a/src/headers/www_authenticate/challenge/bearer.rs b/src/headers/www_authenticate/challenge/bearer.rs deleted file mode 100644 index 04d75f4e8..000000000 --- a/src/headers/www_authenticate/challenge/bearer.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::str; -use std::fmt; -use std::default::Default; - -use bytes::{BufMut, Bytes, BytesMut}; -use actix_web::http::StatusCode; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; - -use super::Challenge; - -/// Bearer authorization error types, described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3.1) -#[derive(Debug, Copy, Clone)] -pub enum Error { - /// The request is missing a required parameter, includes an unsupported parameter - /// or parameter value, repeats the same parameter, uses more than one method - /// for including an access token, or is otherwise malformed. - InvalidRequest, - - /// The access token provided is expired, revoked, malformed, or invalid for other reasons. - InvalidToken, - - /// The request requires higher privileges than provided by the access token. - InsufficientScope, -} - -impl Error { - pub fn status_code(&self) -> StatusCode { - match *self { - Error::InvalidRequest => StatusCode::BAD_REQUEST, - Error::InvalidToken => StatusCode::UNAUTHORIZED, - Error::InsufficientScope => StatusCode::FORBIDDEN, - } - } - - fn as_str(&self) -> &'static str { - match *self { - Error::InvalidRequest => "invalid_request", - Error::InvalidToken => "invalid_token", - Error::InsufficientScope => "insufficient_scope", - } - } -} - -/// Challenge for `WWW-Authenticate` header with HTTP Bearer auth scheme, -/// described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3) -#[derive(Debug, Clone)] -pub struct Bearer { - pub scope: Option, - pub realm: Option, - pub error: Option, - pub error_description: Option, - /// It is up to implementor to provide correct absolute URI - pub error_uri: Option, -} - -impl Challenge for Bearer { - fn to_bytes(&self) -> Bytes { - let desc_uri_required = - self.error_description.as_ref().map_or(0, |desc| desc.len() + 20) + - self.error_uri.as_ref().map_or(0, |url| url.len() + 12); - let capacity = 6 + - self.realm.as_ref().map_or(0, |realm| realm.len() + 9) + - self.scope.as_ref().map_or(0, |scope| scope.len() + 9) + - desc_uri_required; - let mut buffer = BytesMut::with_capacity(capacity); - buffer.put("Bearer"); - - if let Some(ref realm) = self.realm { - buffer.put(" realm=\""); - buffer.put(realm); - buffer.put("\""); - } - - if let Some(ref scope) = self.scope { - buffer.put(" scope=\""); - buffer.put(scope); - buffer.put("\""); - } - - if let Some(ref error) = self.error { - let error_repr = error.as_str(); - let remaining = buffer.remaining_mut(); - let required = desc_uri_required + error_repr.len() + 9; // 9 is for `" error=\"\""` - if remaining < required { - buffer.reserve(required); - } - buffer.put(" error=\""); - buffer.put(error_repr); - buffer.put("\"") - } - - if let Some(ref error_description) = self.error_description { - buffer.put(" error_description=\""); - buffer.put(error_description); - buffer.put("\""); - } - - if let Some(ref error_uri) = self.error_uri { - buffer.put(" error_uri=\""); - buffer.put(error_uri); - buffer.put("\""); - } - - buffer.freeze() - } -} - -impl Default for Bearer { - fn default() -> Self { - Bearer { - scope: None, - realm: None, - error: None, - error_description: None, - error_uri: None, - } - } -} - -impl fmt::Display for Bearer { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - let bytes = self.to_bytes(); - let repr = str::from_utf8(&bytes) - // Should not happen since challenges are crafted manually - // from `&'static str`'s and Strings - .map_err(|_| fmt::Error)?; - - f.write_str(repr) - } -} - -impl IntoHeaderValue for Bearer { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result::Error> { - HeaderValue::from_shared(self.to_bytes()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn to_bytes() { - let b = Bearer { - scope: None, - realm: None, - error: Some(Error::InvalidToken), - error_description: Some(String::from("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found")), - error_uri: None, - }; - assert_eq!("Bearer error=\"invalid_token\" error_description=\"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found\"", - format!("{}", b)); - } -} diff --git a/src/headers/www_authenticate/challenge/bearer/builder.rs b/src/headers/www_authenticate/challenge/bearer/builder.rs new file mode 100644 index 000000000..b4bf11455 --- /dev/null +++ b/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/src/headers/www_authenticate/challenge/bearer/challenge.rs b/src/headers/www_authenticate/challenge/bearer/challenge.rs new file mode 100644 index 000000000..8a03372f3 --- /dev/null +++ b/src/headers/www_authenticate/challenge/bearer/challenge.rs @@ -0,0 +1,132 @@ +use std::borrow::Cow; +use std::fmt; +use std::str; + +use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use bytes::{BufMut, Bytes, BytesMut}; + +use super::super::Challenge; +use super::{BearerBuilder, Error}; +use crate::utils; + +/// Challenge for [`WWW-Authenticate`] header with HTTP Bearer auth scheme, +/// described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3) +/// +/// ## Example +/// +/// ```rust +/// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; +/// use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer, Error}; +/// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate; +/// +/// fn index(_req: HttpRequest) -> HttpResponse { +/// let challenge = Bearer::build() +/// .realm("example") +/// .scope("openid profile email") +/// .error(Error::InvalidToken) +/// .error_description("The access token expired") +/// .error_uri("http://example.org") +/// .finish(); +/// +/// HttpResponse::Unauthorized().set(WwwAuthenticate(challenge)).finish() +/// } +/// ``` +/// +/// [`WWW-Authenticate`]: ../struct.WwwAuthenticate.html +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)] +pub struct Bearer { + pub(crate) scope: Option>, + 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 + /// + /// ```rust + /// # use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer}; + /// let challenge = Bearer::build() + /// .realm("Restricted area") + /// .scope("openid profile email") + /// .finish(); + /// ``` + pub fn build() -> BearerBuilder { + BearerBuilder::default() + } +} + +#[doc(hidden)] +impl Challenge for Bearer { + fn to_bytes(&self) -> Bytes { + let desc_uri_required = self.error_description.as_ref().map_or(0, |desc| desc.len() + 20) + + self.error_uri.as_ref().map_or(0, |url| url.len() + 12); + let capacity = 6 + + self.realm.as_ref().map_or(0, |realm| realm.len() + 9) + + self.scope.as_ref().map_or(0, |scope| scope.len() + 9) + + desc_uri_required; + let mut buffer = BytesMut::with_capacity(capacity); + buffer.put("Bearer"); + + if let Some(ref realm) = self.realm { + buffer.put(" realm=\""); + utils::put_cow(&mut buffer, realm); + buffer.put("\""); + } + + if let Some(ref scope) = self.scope { + buffer.put(" scope=\""); + utils::put_cow(&mut buffer, scope); + buffer.put("\""); + } + + if let Some(ref error) = self.error { + let error_repr = error.as_str(); + let remaining = buffer.remaining_mut(); + let required = desc_uri_required + error_repr.len() + 9; // 9 is for `" error=\"\""` + if remaining < required { + buffer.reserve(required); + } + buffer.put(" error=\""); + buffer.put(error_repr); + buffer.put("\"") + } + + if let Some(ref error_description) = self.error_description { + buffer.put(" error_description=\""); + utils::put_cow(&mut buffer, error_description); + buffer.put("\""); + } + + if let Some(ref error_uri) = self.error_uri { + buffer.put(" error_uri=\""); + utils::put_cow(&mut buffer, error_uri); + buffer.put("\""); + } + + buffer.freeze() + } +} + +impl fmt::Display for Bearer { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let bytes = self.to_bytes(); + let repr = str::from_utf8(&bytes) + // Should not happen since challenges are crafted manually + // from `&'static str`'s and Strings + .map_err(|_| fmt::Error)?; + + f.write_str(repr) + } +} + +impl IntoHeaderValue for Bearer { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result::Error> { + HeaderValue::from_shared(self.to_bytes()) + } +} diff --git a/src/headers/www_authenticate/challenge/bearer/errors.rs b/src/headers/www_authenticate/challenge/bearer/errors.rs new file mode 100644 index 000000000..5509fccec --- /dev/null +++ b/src/headers/www_authenticate/challenge/bearer/errors.rs @@ -0,0 +1,48 @@ +use std::fmt; + +use actix_web::http::StatusCode; + +/// Bearer authorization error types, described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3.1) +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum Error { + /// The request is missing a required parameter, includes an unsupported parameter + /// or parameter value, repeats the same parameter, uses more than one method + /// for including an access token, or is otherwise malformed. + InvalidRequest, + + /// The access token provided is expired, revoked, malformed, or invalid for other reasons. + InvalidToken, + + /// The request requires higher privileges than provided by the access token. + InsufficientScope, +} + +impl Error { + /// Returns [HTTP status code] suitable for current error type. + /// + /// [HTTP status code]: `actix_web::http::StatusCode` + #[allow(clippy::trivially_copy_pass_by_ref)] + pub fn status_code(&self) -> StatusCode { + match self { + Error::InvalidRequest => StatusCode::BAD_REQUEST, + Error::InvalidToken => StatusCode::UNAUTHORIZED, + Error::InsufficientScope => StatusCode::FORBIDDEN, + } + } + + #[doc(hidden)] + #[allow(clippy::trivially_copy_pass_by_ref)] + pub fn as_str(&self) -> &str { + match self { + Error::InvalidRequest => "invalid_request", + Error::InvalidToken => "invalid_token", + Error::InsufficientScope => "insufficient_scope", + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.as_str()) + } +} diff --git a/src/headers/www_authenticate/challenge/bearer/mod.rs b/src/headers/www_authenticate/challenge/bearer/mod.rs new file mode 100644 index 000000000..d51237c45 --- /dev/null +++ b/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/src/headers/www_authenticate/challenge/bearer/tests.rs b/src/headers/www_authenticate/challenge/bearer/tests.rs new file mode 100644 index 000000000..03f088e17 --- /dev/null +++ b/src/headers/www_authenticate/challenge/bearer/tests.rs @@ -0,0 +1,14 @@ +use super::*; + +#[test] +fn to_bytes() { + let b = Bearer::build() + .error(Error::InvalidToken) + .error_description("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found") + .finish(); + + assert_eq!( + "Bearer error=\"invalid_token\" error_description=\"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found\"", + format!("{}", b) + ); +} diff --git a/src/headers/www_authenticate/challenge/mod.rs b/src/headers/www_authenticate/challenge/mod.rs index 372de9884..ce70f4fe2 100644 --- a/src/headers/www_authenticate/challenge/mod.rs +++ b/src/headers/www_authenticate/challenge/mod.rs @@ -1,12 +1,13 @@ use std::fmt::{Debug, Display}; -use bytes::Bytes; 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 { +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/src/headers/www_authenticate/header.rs b/src/headers/www_authenticate/header.rs index d439ea97b..6f08c935f 100644 --- a/src/headers/www_authenticate/header.rs +++ b/src/headers/www_authenticate/header.rs @@ -1,36 +1,18 @@ -use actix_web::{HttpMessage}; 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) /// -/// `WWW-Authenticate` header is generic over [Challenge](./trait.Challenge.html) -/// -/// # Example -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate actix_web_httpauth; -/// -/// use actix_web::{HttpRequest, HttpResponse}; -/// use actix_web::http::StatusCode; -/// use actix_web_httpauth::headers::www_authenticate::{WWWAuthenticate}; -/// use actix_web_httpauth::headers::www_authenticate::basic::Basic; -/// -/// fn handler(req: HttpRequest) -> HttpResponse { -/// let challenge = Basic { -/// realm: Some("Restricted area".to_string()), -/// }; -/// req.build_response(StatusCode::UNAUTHORIZED) -/// .set(WWWAuthenticate(challenge)) -/// .finish() -/// } -/// ``` -pub struct WWWAuthenticate(pub C); +/// 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 { +impl Header for WwwAuthenticate { fn name() -> HeaderName { WWW_AUTHENTICATE } @@ -40,7 +22,7 @@ impl Header for WWWAuthenticate { } } -impl IntoHeaderValue for WWWAuthenticate { +impl IntoHeaderValue for WwwAuthenticate { type Error = ::Error; fn try_into(self) -> Result::Error> { diff --git a/src/headers/www_authenticate/mod.rs b/src/headers/www_authenticate/mod.rs index 3c874a48a..6262f050a 100644 --- a/src/headers/www_authenticate/mod.rs +++ b/src/headers/www_authenticate/mod.rs @@ -1,7 +1,9 @@ +//! `WWW-Authenticate` header and various auth challenges + mod challenge; mod header; -pub use self::header::WWWAuthenticate; -pub use self::challenge::Challenge; pub use self::challenge::basic; pub use self::challenge::bearer; +pub use self::challenge::Challenge; +pub use self::header::WwwAuthenticate; diff --git a/src/lib.rs b/src/lib.rs index f97de92c2..8ada78c6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,21 @@ //! HTTP Authorization support for [actix-web](https://actix.rs) framework. //! -//! Provides [`Authorization`](./headers/authorization/struct.Authorization.html) -//! and [`WWW-Authenticate`](./headers/www_authenticate/struct.WWWAuthenticate.html) headers, -//! and `actix-web` extractors for an `Authorization` header. +//! Provides [Authorization] and [WWW-Authenticate] headers, +//! and [extractors] for an [Authorization] header. +//! +//! ## Supported schemes +//! +//! * `Basic`, as defined in [RFC7617](https://tools.ietf.org/html/rfc7617) +//! * `Bearer`, as defined in [RFC6750](https://tools.ietf.org/html/rfc6750) +//! +//! [Authorization]: `crate::headers::authorization::Authorization` +//! [WWW-Authenticate]: `crate::headers::www_authenticate::WwwAuthenticate` +//! [extractors]: https://actix.rs/docs/extractors/ +#![forbid(bare_trait_objects)] +#![forbid(missing_docs)] #![cfg_attr(feature = "nightly", feature(test))] -#[cfg(feature = "nightly")] extern crate test; -extern crate actix_web; -extern crate bytes; -extern crate base64; - -pub mod headers; pub mod extractors; +pub mod headers; +mod utils; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 000000000..e6b7297e9 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,13 @@ +use std::borrow::Cow; + +use bytes::{BufMut, BytesMut}; + +// `bytes::Buf` is not implemented for `Cow<'static, str>`, implementing it by ourselves. +#[inline] +#[allow(clippy::ptr_arg)] // Totally okay to accept the reference to Cow here +pub fn put_cow(buf: &mut BytesMut, value: &Cow<'static, str>) { + match value { + Cow::Borrowed(str) => buf.put(str), + Cow::Owned(ref string) => buf.put(string), + } +} From 1c1e7f672b7fad74dee4982e37804fc09898fccf Mon Sep 17 00:00:00 2001 From: svartalf Date: Wed, 15 May 2019 17:47:49 +0300 Subject: [PATCH 20/36] Badges! --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e64d18105..7f78ccf13 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # 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) ![Build Status](https://travis-ci.org/svartalf/actix-web-httpauth.svg?branch=master) -![Docs](https://docs.rs/actix-web-httpauth/badge.svg) -![Crates.io](https://img.shields.io/crates/v/actix-web-httpauth.svg) +![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. @@ -13,3 +14,7 @@ and can be used both in middlewares and request handlers, check the `examples/` * [Basic](https://tools.ietf.org/html/rfc7617) * [Bearer](https://tools.ietf.org/html/rfc6750) + +## Donations + +If you appreciate my work and want to support me, you can do it [here](https://svartalf.info/donate/). From cac6894ddd20e4b58a586581e4c692c7c0f80c2d Mon Sep 17 00:00:00 2001 From: svartalf Date: Fri, 17 May 2019 17:28:57 +0300 Subject: [PATCH 21/36] Experimental middleware for HTTP auth -- alpha.2 version --- Cargo.toml | 8 +- examples/middleware_basic.rs | 89 ------ rustfmt.toml | 4 +- src/extractors/basic.rs | 63 ++-- src/extractors/bearer.rs | 70 +++-- src/extractors/config.rs | 12 +- src/extractors/mod.rs | 25 ++ src/headers/authorization/header.rs | 13 +- src/headers/authorization/scheme/basic.rs | 23 +- src/headers/authorization/scheme/bearer.rs | 12 +- src/headers/authorization/scheme/mod.rs | 7 +- .../www_authenticate/challenge/basic.rs | 8 +- .../challenge/bearer/challenge.rs | 17 +- .../challenge/bearer/errors.rs | 13 +- .../challenge/bearer/tests.rs | 4 +- src/headers/www_authenticate/challenge/mod.rs | 4 +- src/headers/www_authenticate/header.rs | 8 +- src/lib.rs | 13 +- src/middleware.rs | 297 ++++++++++++++++++ src/utils.rs | 3 +- 20 files changed, 497 insertions(+), 196 deletions(-) delete mode 100644 examples/middleware_basic.rs create mode 100644 src/middleware.rs diff --git a/Cargo.toml b/Cargo.toml index 782e5cb38..68b78922e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.3.0-alpha.1" +version = "0.3.0-alpha.2" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -15,13 +15,11 @@ edition = "2018" [dependencies] actix-web = { version = "1.0.0-beta.5", default_features = false } +futures = "0.1" +actix-service = "0.4.0" bytes = "0.4" base64 = "0.10" -[dev-dependencies] -futures = "0.1.27" -actix-service = "0.4.0" - [features] default = [] nightly = [] diff --git a/examples/middleware_basic.rs b/examples/middleware_basic.rs deleted file mode 100644 index a71859ed5..000000000 --- a/examples/middleware_basic.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::borrow::Cow; -use std::io; - -use actix_service::{Service, Transform}; -use actix_web::{dev, web, App, Error, HttpRequest, HttpServer}; -use futures::future::{self, Either, FutureResult}; -use futures::Poll; - -use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; -use actix_web_httpauth::extractors::AuthenticationError; - -struct Auth(Config); - -impl Transform for Auth -where - S: Service, Error = Error>, - S::Future: 'static, -{ - type Request = dev::ServiceRequest; - type Response = dev::ServiceResponse; - type Error = Error; - type Transform = AuthMiddleware; - type InitError = (); - type Future = FutureResult; - - fn new_transform(&self, service: S) -> Self::Future { - future::ok(AuthMiddleware { - service, - auth: self.0.clone(), - }) - } -} - -struct AuthMiddleware { - service: S, - auth: Config, -} - -impl AuthMiddleware { - fn valid_user(credentials: &BasicAuth) -> bool { - let user_id = credentials.user_id(); - let password = credentials.password(); - - user_id == "Alladin" && password == Some(&Cow::Borrowed("open sesame")) - } -} - -impl Service for AuthMiddleware -where - S: Service, Error = Error>, - S::Future: 'static, -{ - type Request = dev::ServiceRequest; - type Response = dev::ServiceResponse; - type Error = Error; - type Future = Either>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.service.poll_ready() - } - - fn call(&mut self, mut req: dev::ServiceRequest) -> Self::Future { - let auth = BasicAuth::from_service_request(&mut req, &self.auth); - - match auth { - Ok(ref credentials) if Self::valid_user(credentials) => Either::A(self.service.call(req)), - Ok(..) => { - let challenge = self.auth.as_ref().clone(); - let error = AuthenticationError::new(challenge); - Either::B(future::err(Self::Error::from(error))) - } - Err(e) => Either::B(future::err(e.into())), - } - } -} - -fn index(_req: HttpRequest) -> String { - "Hello, authorized user!".to_string() -} - -fn main() -> io::Result<()> { - HttpServer::new(|| { - let config = Config::default().realm("WallyWorld"); - - App::new().wrap(Auth(config)).service(web::resource("/").to(index)) - }) - .bind("127.0.0.1:8088")? - .run() -} diff --git a/rustfmt.toml b/rustfmt.toml index 599ac809c..f944c9712 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -2,8 +2,8 @@ unstable_features = true edition = "2018" version = "Two" wrap_comments = true -comment_width = 120 -max_width = 120 +comment_width = 80 +max_width = 80 merge_imports = false newline_style = "Unix" struct_lit_single_line = false diff --git a/src/extractors/basic.rs b/src/extractors/basic.rs index 98f62e398..3ad5deb1b 100644 --- a/src/extractors/basic.rs +++ b/src/extractors/basic.rs @@ -6,8 +6,9 @@ use actix_web::dev::{Payload, ServiceRequest}; use actix_web::http::header::Header; use actix_web::{FromRequest, HttpRequest}; -use super::config::ExtractorConfig; +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; @@ -15,15 +16,16 @@ use crate::headers::www_authenticate::basic::Basic as Challenge; /// used for [`WWW-Authenticate`] header later. /// /// [`BasicAuth`]: ./struct.BasicAuth.html -/// [`WWW-Authenticate`]: ../../headers/www_authenticate/struct.WwwAuthenticate.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). + /// 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>, @@ -39,7 +41,7 @@ impl AsRef for Config { } } -impl ExtractorConfig for Config { +impl AuthExtractorConfig for Config { type Inner = Challenge; fn into_inner(self) -> Self::Inner { @@ -61,7 +63,8 @@ impl ExtractorConfig for Config { /// ``` /// /// If authentication fails, this extractor fetches the [`Config`] instance -/// from the [app data] in order to properly form the `WWW-Authenticate` response header. +/// from the [app data] in order to properly form the `WWW-Authenticate` +/// response header. /// /// ## Example /// @@ -95,27 +98,6 @@ impl BasicAuth { pub fn password(&self) -> Option<&Cow<'static, str>> { self.0.password() } - - /// Try to parse actix-web' `ServiceRequest` and fetch the `BasicAuth` from it. - /// - /// ## Warning - /// - /// This function is used right now for middleware creation only - /// and might change or be totally removed, - /// depends on `actix-web = "1.0"` release changes. - /// - /// This issue will be resolved in the `0.3.0` release. - /// Before that -- brace yourselves! - pub fn from_service_request(req: &mut ServiceRequest, config: &Config) -> ::Future { - Authorization::::parse(&req) - .map(|auth| BasicAuth(auth.into_scheme())) - .map_err(|_| { - // TODO: debug! the original error - let challenge = config.clone().into_inner(); - - AuthenticationError::new(challenge) - }) - } } impl FromRequest for BasicAuth { @@ -123,13 +105,36 @@ impl FromRequest for BasicAuth { type Config = Config; type Error = AuthenticationError; - fn from_request(req: &HttpRequest, _: &mut Payload) -> ::Future { + fn from_request( + req: &HttpRequest, + _: &mut Payload, + ) -> ::Future { Authorization::::parse(req) .map(|auth| BasicAuth(auth.into_scheme())) .map_err(|_| { // TODO: debug! the original error let challenge = req - .get_app_data::() + .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 = Result; + + fn from_service_request(req: &ServiceRequest) -> Self::Future { + 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); diff --git a/src/extractors/bearer.rs b/src/extractors/bearer.rs index 0b88e63a0..a735954c8 100644 --- a/src/extractors/bearer.rs +++ b/src/extractors/bearer.rs @@ -7,8 +7,9 @@ use actix_web::dev::{Payload, ServiceRequest}; use actix_web::http::header::Header; use actix_web::{FromRequest, HttpRequest}; -use super::config::ExtractorConfig; +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; @@ -20,8 +21,9 @@ 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. + /// 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 @@ -29,8 +31,8 @@ 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). + /// 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 @@ -43,7 +45,7 @@ impl AsRef for Config { } } -impl ExtractorConfig for Config { +impl AuthExtractorConfig for Config { type Inner = bearer::Bearer; fn into_inner(self) -> Self::Inner { @@ -64,7 +66,8 @@ impl ExtractorConfig for Config { /// ``` /// /// If authentication fails, this extractor fetches the [`Config`] instance -/// from the [app data] in order to properly form the `WWW-Authenticate` response header. +/// from the [app data] in order to properly form the `WWW-Authenticate` +/// response header. /// /// ## Example /// @@ -78,7 +81,11 @@ impl ExtractorConfig for Config { /// /// fn main() { /// let app = App::new() -/// .data(Config::default().realm("Restricted area").scope("email photo")) +/// .data( +/// Config::default() +/// .realm("Restricted area") +/// .scope("email photo"), +/// ) /// .service(web::resource("/index.html").route(web::get().to(index))); /// } /// ``` @@ -90,27 +97,6 @@ impl BearerAuth { pub fn token(&self) -> &str { self.0.token() } - - /// Try to parse actix-web' `ServiceRequest` and fetch the `BasicAuth` from it. - /// - /// ## Warning - /// - /// This function is used right now for middleware creation only - /// and might change or be totally removed, - /// depends on `actix-web = "1.0"` release changes. - /// - /// This issue will be resolved in the `0.3.0` release. - /// Before that -- brace yourselves! - pub fn from_service_request(req: &mut ServiceRequest, config: &Config) -> ::Future { - authorization::Authorization::::parse(&req) - .map(|auth| BearerAuth(auth.into_scheme())) - .map_err(|_| { - // TODO: debug! the original error - let challenge = config.clone().into_inner(); - - AuthenticationError::new(challenge) - }) - } } impl FromRequest for BearerAuth { @@ -118,7 +104,10 @@ impl FromRequest for BearerAuth { type Future = Result; type Error = AuthenticationError; - fn from_request(req: &HttpRequest, _payload: &mut Payload) -> ::Future { + fn from_request( + req: &HttpRequest, + _payload: &mut Payload, + ) -> ::Future { authorization::Authorization::::parse(req) .map(|auth| BearerAuth(auth.into_scheme())) .map_err(|_| { @@ -132,11 +121,30 @@ impl FromRequest for BearerAuth { } } +impl AuthExtractor for BearerAuth { + type Future = Result; + type Error = AuthenticationError; + + fn from_service_request(req: &ServiceRequest) -> Self::Future { + 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. + /// 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); diff --git a/src/extractors/config.rs b/src/extractors/config.rs index 721cf8f5b..2faf869d9 100644 --- a/src/extractors/config.rs +++ b/src/extractors/config.rs @@ -1,15 +1,21 @@ use super::AuthenticationError; use crate::headers::www_authenticate::Challenge; -pub trait ExtractorConfig { +/// 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> +impl From for AuthenticationError<::Inner> where - T: ExtractorConfig, + T: AuthExtractorConfig, { fn from(config: T) -> Self { AuthenticationError::new(config.into_inner()) diff --git a/src/extractors/mod.rs b/src/extractors/mod.rs index 797c99713..062efba2d 100644 --- a/src/extractors/mod.rs +++ b/src/extractors/mod.rs @@ -1,8 +1,33 @@ //! Type-safe authentication information extractors +use actix_web::dev::ServiceRequest; +use actix_web::Error; +use futures::IntoFuture; + 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: IntoFuture; + + /// Parse the authentication credentials from the actix' `ServiceRequest`. + fn from_service_request(req: &ServiceRequest) -> Self::Future; +} diff --git a/src/headers/authorization/header.rs b/src/headers/authorization/header.rs index 740af59b5..d6f051124 100644 --- a/src/headers/authorization/header.rs +++ b/src/headers/authorization/header.rs @@ -1,7 +1,9 @@ use std::fmt; use actix_web::error::ParseError; -use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION}; +use actix_web::http::header::{ + Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION, +}; use actix_web::HttpMessage; use crate::headers::authorization::scheme::Scheme; @@ -14,7 +16,8 @@ use crate::headers::authorization::scheme::Scheme; /// 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). +/// `Authorization` header is generic over [authentication +/// scheme](./trait.Scheme.html). /// /// # Example /// @@ -35,7 +38,8 @@ impl Authorization where S: Scheme, { - /// Consumes `Authorization` header and returns inner [`Scheme`] implementation. + /// Consumes `Authorization` header and returns inner [`Scheme`] + /// implementation. /// /// [`Scheme`]: ./trait.Scheme.html pub fn into_scheme(self) -> S { @@ -77,7 +81,8 @@ impl Header for Authorization { } fn parse(msg: &T) -> Result { - let header = msg.headers().get(AUTHORIZATION).ok_or(ParseError::Header)?; + let header = + msg.headers().get(AUTHORIZATION).ok_or(ParseError::Header)?; let scheme = S::parse(header).map_err(|_| ParseError::Header)?; Ok(Authorization(scheme)) diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 06e8aabc2..6c91d6009 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use std::fmt; use std::str; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use actix_web::http::header::{ + HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, +}; use base64; use bytes::{BufMut, BytesMut}; @@ -18,7 +20,8 @@ pub struct Basic { } impl Basic { - /// Creates `Basic` credentials with provided `user_id` and optional `password`. + /// Creates `Basic` credentials with provided `user_id` and optional + /// `password`. /// /// ## Example /// @@ -102,15 +105,19 @@ impl IntoHeaderValue for Basic { type Error = InvalidHeaderValueBytes; 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())); + let mut credentials = BytesMut::with_capacity( + self.user_id.len() + + 1 + + self.password.as_ref().map_or(0, |pwd| pwd.len()), + ); utils::put_cow(&mut credentials, &self.user_id); credentials.put_u8(b':'); if let Some(ref password) = self.password { utils::put_cow(&mut credentials, password); } - // TODO: It would be nice not to allocate new `String` here but write directly to `value` + // TODO: It would be nice not to allocate new `String` here but write + // directly to `value` let encoded = base64::encode(&credentials); let mut value = BytesMut::with_capacity(6 + encoded.len()); value.put("Basic "); @@ -127,7 +134,8 @@ mod tests { #[test] fn test_parse_header() { - let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + let value = + HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); let scheme = Basic::parse(&value); assert!(scheme.is_ok()); @@ -205,7 +213,8 @@ mod benches { #[bench] fn bench_parsing(b: &mut Bencher) { - let value = HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + let value = + HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); b.iter(|| Basic::parse(&value)); } diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs index c350c1deb..78b532a58 100644 --- a/src/headers/authorization/scheme/bearer.rs +++ b/src/headers/authorization/scheme/bearer.rs @@ -1,7 +1,9 @@ use std::borrow::Cow; use std::fmt; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use actix_web::http::header::{ + HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, +}; use bytes::{BufMut, BytesMut}; use crate::headers::authorization::errors::ParseError; @@ -10,7 +12,8 @@ use crate::utils; /// Credentials for `Bearer` authentication scheme, defined in [RFC6750](https://tools.ietf.org/html/rfc6750) /// -/// Should be used in combination with [`Authorization`](./struct.Authorization.html) header. +/// Should be used in combination with +/// [`Authorization`](./struct.Authorization.html) header. #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] pub struct Bearer { token: Cow<'static, str>, @@ -130,6 +133,9 @@ mod tests { let result = bearer.try_into(); assert!(result.is_ok()); - assert_eq!(result.unwrap(), HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM")); + assert_eq!( + result.unwrap(), + HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM") + ); } } diff --git a/src/headers/authorization/scheme/mod.rs b/src/headers/authorization/scheme/mod.rs index 5832965f3..1ab534c93 100644 --- a/src/headers/authorization/scheme/mod.rs +++ b/src/headers/authorization/scheme/mod.rs @@ -7,8 +7,11 @@ 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 { +/// 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/src/headers/www_authenticate/challenge/basic.rs b/src/headers/www_authenticate/challenge/basic.rs index c3c4f22cb..86c637c98 100644 --- a/src/headers/www_authenticate/challenge/basic.rs +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -5,7 +5,9 @@ use std::default::Default; use std::fmt; use std::str; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use actix_web::http::header::{ + HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, +}; use bytes::{BufMut, Bytes, BytesMut}; use super::Challenge; @@ -24,7 +26,9 @@ use crate::utils; /// fn index(_req: HttpRequest) -> HttpResponse { /// let challenge = Basic::with_realm("Restricted area"); /// -/// HttpResponse::Unauthorized().set(WwwAuthenticate(challenge)).finish() +/// HttpResponse::Unauthorized() +/// .set(WwwAuthenticate(challenge)) +/// .finish() /// } /// ``` /// diff --git a/src/headers/www_authenticate/challenge/bearer/challenge.rs b/src/headers/www_authenticate/challenge/bearer/challenge.rs index 8a03372f3..025b0e585 100644 --- a/src/headers/www_authenticate/challenge/bearer/challenge.rs +++ b/src/headers/www_authenticate/challenge/bearer/challenge.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use std::fmt; use std::str; -use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes}; +use actix_web::http::header::{ + HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, +}; use bytes::{BufMut, Bytes, BytesMut}; use super::super::Challenge; @@ -16,7 +18,9 @@ use crate::utils; /// /// ```rust /// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; -/// use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer, Error}; +/// use actix_web_httpauth::headers::www_authenticate::bearer::{ +/// Bearer, Error, +/// }; /// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate; /// /// fn index(_req: HttpRequest) -> HttpResponse { @@ -28,7 +32,9 @@ use crate::utils; /// .error_uri("http://example.org") /// .finish(); /// -/// HttpResponse::Unauthorized().set(WwwAuthenticate(challenge)).finish() +/// HttpResponse::Unauthorized() +/// .set(WwwAuthenticate(challenge)) +/// .finish() /// } /// ``` /// @@ -62,7 +68,10 @@ impl Bearer { #[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) + 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) diff --git a/src/headers/www_authenticate/challenge/bearer/errors.rs b/src/headers/www_authenticate/challenge/bearer/errors.rs index 5509fccec..7bea8d4a4 100644 --- a/src/headers/www_authenticate/challenge/bearer/errors.rs +++ b/src/headers/www_authenticate/challenge/bearer/errors.rs @@ -5,15 +5,18 @@ 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. + /// The request is missing a required parameter, includes an unsupported + /// parameter or parameter value, repeats the same parameter, uses more + /// than one method for including an access token, or is otherwise + /// malformed. InvalidRequest, - /// The access token provided is expired, revoked, malformed, or invalid for other reasons. + /// The access token provided is expired, revoked, malformed, or invalid + /// for other reasons. InvalidToken, - /// The request requires higher privileges than provided by the access token. + /// The request requires higher privileges than provided by the access + /// token. InsufficientScope, } diff --git a/src/headers/www_authenticate/challenge/bearer/tests.rs b/src/headers/www_authenticate/challenge/bearer/tests.rs index 03f088e17..015c07d33 100644 --- a/src/headers/www_authenticate/challenge/bearer/tests.rs +++ b/src/headers/www_authenticate/challenge/bearer/tests.rs @@ -4,7 +4,9 @@ use super::*; fn to_bytes() { let b = Bearer::build() .error(Error::InvalidToken) - .error_description("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found") + .error_description( + "Subject 8740827c-2e0a-447b-9716-d73042e4039d not found", + ) .finish(); assert_eq!( diff --git a/src/headers/www_authenticate/challenge/mod.rs b/src/headers/www_authenticate/challenge/mod.rs index ce70f4fe2..619c5ef51 100644 --- a/src/headers/www_authenticate/challenge/mod.rs +++ b/src/headers/www_authenticate/challenge/mod.rs @@ -7,7 +7,9 @@ pub mod basic; pub mod bearer; /// Authentication challenge for `WWW-Authenticate` header. -pub trait Challenge: IntoHeaderValue + Debug + Display + Clone + Send + Sync { +pub trait Challenge: + IntoHeaderValue + Debug + Display + Clone + Send + Sync +{ /// Converts the challenge into a bytes suitable for HTTP transmission. fn to_bytes(&self) -> Bytes; } diff --git a/src/headers/www_authenticate/header.rs b/src/headers/www_authenticate/header.rs index 6f08c935f..092b7593b 100644 --- a/src/headers/www_authenticate/header.rs +++ b/src/headers/www_authenticate/header.rs @@ -1,5 +1,7 @@ use actix_web::error::ParseError; -use actix_web::http::header::{Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE}; +use actix_web::http::header::{ + Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE, +}; use actix_web::HttpMessage; use super::Challenge; @@ -7,8 +9,8 @@ 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. +/// 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); diff --git a/src/lib.rs b/src/lib.rs index 8ada78c6a..422eace13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ //! HTTP Authorization support for [actix-web](https://actix.rs) framework. //! -//! Provides [Authorization] and [WWW-Authenticate] headers, -//! and [extractors] for an [Authorization] header. +//! Provides: +//! * typed [Authorization] and [WWW-Authenticate] headers +//! * [extractors] for an [Authorization] header +//! * [middleware] for easier authorization checking //! //! ## Supported schemes //! @@ -11,11 +13,14 @@ //! [Authorization]: `crate::headers::authorization::Authorization` //! [WWW-Authenticate]: `crate::headers::www_authenticate::WwwAuthenticate` //! [extractors]: https://actix.rs/docs/extractors/ +//! [middleware]: ./middleware/ -#![forbid(bare_trait_objects)] -#![forbid(missing_docs)] +#![deny(bare_trait_objects)] +#![deny(missing_docs)] +#![deny(unused)] #![cfg_attr(feature = "nightly", feature(test))] pub mod extractors; pub mod headers; +pub mod middleware; mod utils; diff --git a/src/middleware.rs b/src/middleware.rs new file mode 100644 index 000000000..c2eceb12d --- /dev/null +++ b/src/middleware.rs @@ -0,0 +1,297 @@ +//! HTTP Authentication middleware. + +use std::marker::PhantomData; +use std::rc::Rc; + +use actix_service::{Service, Transform}; +use actix_web::dev::{ServiceRequest, ServiceResponse}; +use actix_web::Error; +use futures::future::{self, FutureResult}; +use futures::{Async, Future, IntoFuture, 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 parsed credentials into it. +pub struct HttpAuthentication +where + T: AuthExtractor, +{ + validator_fn: Rc, + _extractor: PhantomData, +} + +impl HttpAuthentication +where + T: AuthExtractor, + F: FnMut(&mut ServiceRequest, T) -> O, + O: IntoFuture, +{ + /// Construct `HttpAuthentication` middleware + /// with the provided auth extractor `T` and + /// validation callback `F`. + pub fn with_fn(validator_fn: F) -> HttpAuthentication { + HttpAuthentication { + validator_fn: Rc::new(validator_fn), + _extractor: PhantomData, + } + } +} + +impl HttpAuthentication +where + F: FnMut(&mut ServiceRequest, basic::BasicAuth) -> O, + O: IntoFuture, +{ + /// Construct `HttpAuthentication` middleware for the HTTP "Basic" + /// authentication scheme. + /// + /// ## Example + /// + /// ``` + /// # use actix_web::Error; + /// # use actix_web::dev::ServiceRequest; + /// # use futures::future::{self, FutureResult}; + /// # 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. + /// fn validator( + /// req: &mut ServiceRequest, + /// credentials: BasicAuth, + /// ) -> FutureResult<(), Error> { + /// // All users are great and more than welcome! + /// future::ok(()) + /// } + /// + /// let middleware = HttpAuthentication::basic(validator); + /// ``` + pub fn basic(validator_fn: F) -> Self { + Self::with_fn(validator_fn) + } +} + +impl HttpAuthentication +where + F: FnMut(&mut ServiceRequest, bearer::BearerAuth) -> O, + O: IntoFuture, +{ + /// Construct `HttpAuthentication` middleware for the HTTP "Bearer" + /// authentication scheme. + /// ## Example + /// + /// ``` + /// # use actix_web::Error; + /// # use actix_web::dev::ServiceRequest; + /// # use futures::future::{self, FutureResult}; + /// # use actix_web_httpauth::middleware::HttpAuthentication; + /// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth}; + /// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig}; + /// fn validator(req: &mut ServiceRequest, credentials: BearerAuth) -> FutureResult<(), Error> { + /// if credentials.token() == "mF_9.B5f-4.1JqM" { + /// future::ok(()) + /// } 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"); + /// + /// future::err(AuthenticationError::from(config).into()) + /// } + /// } + /// + /// let middleware = HttpAuthentication::bearer(validator); + /// ``` + pub fn bearer(validator_fn: F) -> Self { + Self::with_fn(validator_fn) + } +} + +impl Transform for HttpAuthentication +where + S: Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + S::Future: 'static, + F: Fn(&mut ServiceRequest, T) -> O + 'static, + O: IntoFuture + 'static, + T: AuthExtractor + 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Transform = AuthenticationMiddleware; + type InitError = (); + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + future::ok(AuthenticationMiddleware { + service: Some(service), + validator_fn: self.validator_fn.clone(), + _extractor: PhantomData, + }) + } +} + +#[doc(hidden)] +pub struct AuthenticationMiddleware +where + T: AuthExtractor, +{ + service: Option, + validator_fn: Rc, + _extractor: PhantomData, +} + +impl Service for AuthenticationMiddleware +where + S: Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + S::Future: 'static, + F: Fn(&mut ServiceRequest, T) -> O + 'static, + O: IntoFuture + 'static, + T: AuthExtractor + 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = S::Error; + type Future = Box, Error = Error>>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service + .as_mut() + .expect("AuthenticationMiddleware was called already") + .poll_ready() + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + let validator_fn = self.validator_fn.clone(); + let mut service = self + .service + .take() + .expect("AuthenticationMiddleware was called twice"); + + let f = Extract::new(req) + .and_then(move |(req, credentials)| { + Validate::new(req, validator_fn, credentials) + }) + .and_then(move |req| service.call(req)); + + Box::new(f) + } +} + +struct Extract { + req: Option, + f: Option>>, + _extractor: PhantomData, +} + +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 Item = (ServiceRequest, T); + type Error = Error; + + fn poll(&mut self) -> 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) + .into_future() + .map_err(Into::into); + self.f = Some(Box::new(f)); + } + + let f = self + .f + .as_mut() + .expect("Extraction future should be initialized at this point"); + let credentials = futures::try_ready!(f.poll()); + + let req = self.req.take().expect("Extract future was polled twice!"); + Ok(Async::Ready((req, credentials))) + } +} + +struct Validate { + req: Option, + validation_f: Option>>, + validator_fn: Rc, + credentials: Option, +} + +impl Validate { + pub fn new( + req: ServiceRequest, + validator_fn: Rc, + credentials: T, + ) -> Self { + Validate { + req: Some(req), + credentials: Some(credentials), + validator_fn, + validation_f: None, + } + } +} + +impl Future for Validate +where + F: Fn(&mut ServiceRequest, T) -> O, + O: IntoFuture + 'static, +{ + type Item = ServiceRequest; + type Error = Error; + + fn poll(&mut self) -> Poll { + if self.validation_f.is_none() { + let req = self + .req + .as_mut() + .expect("Unable to get the mutable access to the request"); + let credentials = self + .credentials + .take() + .expect("Validate future was polled in some weird manner"); + let f = (self.validator_fn)(req, credentials).into_future(); + + self.validation_f = Some(Box::new(f)); + } + + let f = self + .validation_f + .as_mut() + .expect("Validation future should exist at this moment"); + // We do not care about returned `Ok(())` + futures::try_ready!(f.poll()); + let req = self.req.take().expect("Validate future was polled already"); + + Ok(Async::Ready(req)) + } +} diff --git a/src/utils.rs b/src/utils.rs index e6b7297e9..e30faedf3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,8 @@ use std::borrow::Cow; use bytes::{BufMut, BytesMut}; -// `bytes::Buf` is not implemented for `Cow<'static, str>`, implementing it by ourselves. +// `bytes::Buf` is not implemented for `Cow<'static, str>`, implementing it by +// ourselves. #[inline] #[allow(clippy::ptr_arg)] // Totally okay to accept the reference to Cow here pub fn put_cow(buf: &mut BytesMut, value: &Cow<'static, str>) { From dc3223104a0cb0bb817a1c0d08c97df06086ba98 Mon Sep 17 00:00:00 2001 From: svartalf Date: Thu, 23 May 2019 16:32:55 +0300 Subject: [PATCH 22/36] Create FUNDING.yml --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..857958524 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: svartalf +open_collective: rust-battery +custom: https://svartalf.info/donate/ From aad60744ed8e177c65bf6cd2a0aecc7858ceff2f Mon Sep 17 00:00:00 2001 From: svartalf Date: Fri, 24 May 2019 14:51:13 +0300 Subject: [PATCH 23/36] Update FUNDING.yml --- .github/FUNDING.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 857958524..e4510bce2 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,2 @@ github: svartalf -open_collective: rust-battery custom: https://svartalf.info/donate/ From 0ebfd790fc8d18aa90427479ba1ce668e856b77e Mon Sep 17 00:00:00 2001 From: svartalf Date: Wed, 5 Jun 2019 18:52:47 +0300 Subject: [PATCH 24/36] Release 0.3.0 --- Cargo.toml | 4 +- README.md | 10 +- src/headers/authorization/scheme/basic.rs | 8 +- src/headers/authorization/scheme/bearer.rs | 3 +- .../www_authenticate/challenge/basic.rs | 2 +- .../challenge/bearer/challenge.rs | 10 +- src/middleware.rs | 122 +++++------------- src/utils.rs | 121 +++++++++++++++-- 8 files changed, 167 insertions(+), 113 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 68b78922e..0e9ee8a07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.3.0-alpha.2" +version = "0.3.0" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -14,7 +14,7 @@ exclude = [".travis.yml", ".gitignore"] edition = "2018" [dependencies] -actix-web = { version = "1.0.0-beta.5", default_features = false } +actix-web = { version = "^1.0", default_features = false } futures = "0.1" actix-service = "0.4.0" bytes = "0.4" diff --git a/README.md b/README.md index 7f78ccf13..51a82e39d 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,19 @@ [![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.3.0/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.3.0) ![Build Status](https://travis-ci.org/svartalf/actix-web-httpauth.svg?branch=master) ![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. -All supported schemas are actix [Extractors](https://docs.rs/actix-web/0.6.7/actix_web/trait.FromRequest.html), -and can be used both in middlewares and request handlers, check the `examples/` folder. +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 diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 6c91d6009..0889044b3 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -10,7 +10,6 @@ use bytes::{BufMut, BytesMut}; use crate::headers::authorization::errors::ParseError; use crate::headers::authorization::Scheme; -use crate::utils; /// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617) #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] @@ -107,13 +106,14 @@ impl IntoHeaderValue for Basic { fn try_into(self) -> Result::Error> { let mut credentials = BytesMut::with_capacity( self.user_id.len() - + 1 + + 1 // ':' + self.password.as_ref().map_or(0, |pwd| pwd.len()), ); - utils::put_cow(&mut credentials, &self.user_id); + + credentials.extend_from_slice(self.user_id.as_bytes()); credentials.put_u8(b':'); if let Some(ref password) = self.password { - utils::put_cow(&mut credentials, password); + credentials.extend_from_slice(password.as_bytes()); } // TODO: It would be nice not to allocate new `String` here but write diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs index 78b532a58..d9eac636d 100644 --- a/src/headers/authorization/scheme/bearer.rs +++ b/src/headers/authorization/scheme/bearer.rs @@ -8,7 +8,6 @@ use bytes::{BufMut, BytesMut}; use crate::headers::authorization::errors::ParseError; use crate::headers::authorization::scheme::Scheme; -use crate::utils; /// Credentials for `Bearer` authentication scheme, defined in [RFC6750](https://tools.ietf.org/html/rfc6750) /// @@ -82,7 +81,7 @@ impl IntoHeaderValue for Bearer { fn try_into(self) -> Result::Error> { let mut buffer = BytesMut::with_capacity(7 + self.token.len()); buffer.put("Bearer "); - utils::put_cow(&mut buffer, &self.token); + buffer.extend_from_slice(self.token.as_bytes()); HeaderValue::from_shared(buffer.freeze()) } diff --git a/src/headers/www_authenticate/challenge/basic.rs b/src/headers/www_authenticate/challenge/basic.rs index 86c637c98..d5699e01a 100644 --- a/src/headers/www_authenticate/challenge/basic.rs +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -85,7 +85,7 @@ impl Challenge for Basic { buffer.put("Basic"); if let Some(ref realm) = self.realm { buffer.put(" realm=\""); - utils::put_cow(&mut buffer, realm); + utils::put_quoted(&mut buffer, realm); buffer.put("\""); } diff --git a/src/headers/www_authenticate/challenge/bearer/challenge.rs b/src/headers/www_authenticate/challenge/bearer/challenge.rs index 025b0e585..92f977dda 100644 --- a/src/headers/www_authenticate/challenge/bearer/challenge.rs +++ b/src/headers/www_authenticate/challenge/bearer/challenge.rs @@ -82,13 +82,13 @@ impl Challenge for Bearer { if let Some(ref realm) = self.realm { buffer.put(" realm=\""); - utils::put_cow(&mut buffer, realm); + utils::put_quoted(&mut buffer, realm); buffer.put("\""); } if let Some(ref scope) = self.scope { buffer.put(" scope=\""); - utils::put_cow(&mut buffer, scope); + utils::put_quoted(&mut buffer, scope); buffer.put("\""); } @@ -100,19 +100,19 @@ impl Challenge for Bearer { buffer.reserve(required); } buffer.put(" error=\""); - buffer.put(error_repr); + utils::put_quoted(&mut buffer, error_repr); buffer.put("\"") } if let Some(ref error_description) = self.error_description { buffer.put(" error_description=\""); - utils::put_cow(&mut buffer, error_description); + utils::put_quoted(&mut buffer, error_description); buffer.put("\""); } if let Some(ref error_uri) = self.error_uri { buffer.put(" error_uri=\""); - utils::put_cow(&mut buffer, error_uri); + utils::put_quoted(&mut buffer, error_uri); buffer.put("\""); } diff --git a/src/middleware.rs b/src/middleware.rs index c2eceb12d..e35efdb30 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -16,27 +16,31 @@ use crate::extractors::{basic, bearer, AuthExtractor}; /// If there is no `Authorization` header in the request, /// this middleware returns an error immediately, /// without calling the `F` callback. -/// Otherwise, it will pass parsed credentials into it. +/// +/// 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. pub struct HttpAuthentication where T: AuthExtractor, { - validator_fn: Rc, + process_fn: Rc, _extractor: PhantomData, } impl HttpAuthentication where T: AuthExtractor, - F: FnMut(&mut ServiceRequest, T) -> O, - O: IntoFuture, + F: FnMut(ServiceRequest, T) -> O, + O: IntoFuture, { /// Construct `HttpAuthentication` middleware /// with the provided auth extractor `T` and /// validation callback `F`. - pub fn with_fn(validator_fn: F) -> HttpAuthentication { + pub fn with_fn(process_fn: F) -> HttpAuthentication { HttpAuthentication { - validator_fn: Rc::new(validator_fn), + process_fn: Rc::new(process_fn), _extractor: PhantomData, } } @@ -44,15 +48,15 @@ where impl HttpAuthentication where - F: FnMut(&mut ServiceRequest, basic::BasicAuth) -> O, - O: IntoFuture, + F: FnMut(ServiceRequest, basic::BasicAuth) -> O, + O: IntoFuture, { /// Construct `HttpAuthentication` middleware for the HTTP "Basic" /// authentication scheme. /// /// ## Example /// - /// ``` + /// ```rust /// # use actix_web::Error; /// # use actix_web::dev::ServiceRequest; /// # use futures::future::{self, FutureResult}; @@ -64,39 +68,40 @@ where /// // it can be extended to query database /// // or to do something else in a async manner. /// fn validator( - /// req: &mut ServiceRequest, - /// credentials: BasicAuth, - /// ) -> FutureResult<(), Error> { + /// req: ServiceRequest, + /// credentials: BasicAuth, + /// ) -> FutureResult { /// // All users are great and more than welcome! - /// future::ok(()) + /// future::ok(req) /// } /// /// let middleware = HttpAuthentication::basic(validator); /// ``` - pub fn basic(validator_fn: F) -> Self { - Self::with_fn(validator_fn) + pub fn basic(process_fn: F) -> Self { + Self::with_fn(process_fn) } } impl HttpAuthentication where - F: FnMut(&mut ServiceRequest, bearer::BearerAuth) -> O, - O: IntoFuture, + F: FnMut(ServiceRequest, bearer::BearerAuth) -> O, + O: IntoFuture, { /// Construct `HttpAuthentication` middleware for the HTTP "Bearer" /// authentication scheme. + /// /// ## Example /// - /// ``` + /// ```rust /// # use actix_web::Error; /// # use actix_web::dev::ServiceRequest; /// # use futures::future::{self, FutureResult}; /// # use actix_web_httpauth::middleware::HttpAuthentication; /// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth}; /// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig}; - /// fn validator(req: &mut ServiceRequest, credentials: BearerAuth) -> FutureResult<(), Error> { + /// fn validator(req: ServiceRequest, credentials: BearerAuth) -> FutureResult { /// if credentials.token() == "mF_9.B5f-4.1JqM" { - /// future::ok(()) + /// future::ok(req) /// } else { /// let config = req.app_data::() /// .map(|data| data.get_ref().clone()) @@ -109,8 +114,8 @@ where /// /// let middleware = HttpAuthentication::bearer(validator); /// ``` - pub fn bearer(validator_fn: F) -> Self { - Self::with_fn(validator_fn) + pub fn bearer(process_fn: F) -> Self { + Self::with_fn(process_fn) } } @@ -122,8 +127,8 @@ where Error = Error, > + 'static, S::Future: 'static, - F: Fn(&mut ServiceRequest, T) -> O + 'static, - O: IntoFuture + 'static, + F: Fn(ServiceRequest, T) -> O + 'static, + O: IntoFuture + 'static, T: AuthExtractor + 'static, { type Request = ServiceRequest; @@ -136,7 +141,7 @@ where fn new_transform(&self, service: S) -> Self::Future { future::ok(AuthenticationMiddleware { service: Some(service), - validator_fn: self.validator_fn.clone(), + process_fn: self.process_fn.clone(), _extractor: PhantomData, }) } @@ -148,7 +153,7 @@ where T: AuthExtractor, { service: Option, - validator_fn: Rc, + process_fn: Rc, _extractor: PhantomData, } @@ -160,8 +165,8 @@ where Error = Error, > + 'static, S::Future: 'static, - F: Fn(&mut ServiceRequest, T) -> O + 'static, - O: IntoFuture + 'static, + F: Fn(ServiceRequest, T) -> O + 'static, + O: IntoFuture + 'static, T: AuthExtractor + 'static, { type Request = ServiceRequest; @@ -177,7 +182,7 @@ where } fn call(&mut self, req: Self::Request) -> Self::Future { - let validator_fn = self.validator_fn.clone(); + let process_fn = self.process_fn.clone(); let mut service = self .service .take() @@ -185,7 +190,7 @@ where let f = Extract::new(req) .and_then(move |(req, credentials)| { - Validate::new(req, validator_fn, credentials) + (process_fn)(req, credentials) }) .and_then(move |req| service.call(req)); @@ -238,60 +243,3 @@ where Ok(Async::Ready((req, credentials))) } } - -struct Validate { - req: Option, - validation_f: Option>>, - validator_fn: Rc, - credentials: Option, -} - -impl Validate { - pub fn new( - req: ServiceRequest, - validator_fn: Rc, - credentials: T, - ) -> Self { - Validate { - req: Some(req), - credentials: Some(credentials), - validator_fn, - validation_f: None, - } - } -} - -impl Future for Validate -where - F: Fn(&mut ServiceRequest, T) -> O, - O: IntoFuture + 'static, -{ - type Item = ServiceRequest; - type Error = Error; - - fn poll(&mut self) -> Poll { - if self.validation_f.is_none() { - let req = self - .req - .as_mut() - .expect("Unable to get the mutable access to the request"); - let credentials = self - .credentials - .take() - .expect("Validate future was polled in some weird manner"); - let f = (self.validator_fn)(req, credentials).into_future(); - - self.validation_f = Some(Box::new(f)); - } - - let f = self - .validation_f - .as_mut() - .expect("Validation future should exist at this moment"); - // We do not care about returned `Ok(())` - futures::try_ready!(f.poll()); - let req = self.req.take().expect("Validate future was polled already"); - - Ok(Async::Ready(req)) - } -} diff --git a/src/utils.rs b/src/utils.rs index e30faedf3..4312663e1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,14 +1,115 @@ -use std::borrow::Cow; +use std::str; -use bytes::{BufMut, BytesMut}; +use bytes::BytesMut; -// `bytes::Buf` is not implemented for `Cow<'static, str>`, implementing it by -// ourselves. -#[inline] -#[allow(clippy::ptr_arg)] // Totally okay to accept the reference to Cow here -pub fn put_cow(buf: &mut BytesMut, value: &Cow<'static, str>) { - match value { - Cow::Borrowed(str) => buf.put(str), - Cow::Owned(ref string) => buf.put(string), +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"); } } From 27a894cab7ab8785160090f0d0898d00e81eedd3 Mon Sep 17 00:00:00 2001 From: svartalf Date: Sat, 8 Jun 2019 00:20:55 +0300 Subject: [PATCH 25/36] Fixing the middleware multiple calls panic --- Cargo.toml | 5 +++-- README.md | 2 +- examples/middleware.rs | 27 +++++++++++++++++++++++++++ src/middleware.rs | 35 +++++++++++++++++++---------------- src/utils.rs | 26 +++++++++++--------------- 5 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 examples/middleware.rs diff --git a/Cargo.toml b/Cargo.toml index 0e9ee8a07..1f48b6bfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.3.0" +version = "0.3.1" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -15,8 +15,9 @@ edition = "2018" [dependencies] actix-web = { version = "^1.0", default_features = false } -futures = "0.1" actix-service = "0.4.0" +futures = "0.1" +futures-locks = "0.3.3" bytes = "0.4" base64 = "0.10" diff --git a/README.md b/README.md index 51a82e39d..5e5b76d30 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![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.3.0/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.3.0) +[![dependency status](https://deps.rs/crate/actix-web-httpauth/0.3.1/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.3.1) ![Build Status](https://travis-ci.org/svartalf/actix-web-httpauth.svg?branch=master) ![Apache 2.0 OR MIT licensed](https://img.shields.io/badge/license-Apache2.0%2FMIT-blue.svg) diff --git a/examples/middleware.rs b/examples/middleware.rs new file mode 100644 index 000000000..468d69de9 --- /dev/null +++ b/examples/middleware.rs @@ -0,0 +1,27 @@ +use actix_web::dev::ServiceRequest; +use actix_web::{middleware, web, App, Error, HttpServer}; + +use futures::future; + +use actix_web_httpauth::extractors::basic::BasicAuth; +use actix_web_httpauth::middleware::HttpAuthentication; + +fn validator( + req: ServiceRequest, + _credentials: BasicAuth, +) -> future::FutureResult { + future::ok(req) +} + +fn main() -> std::io::Result<()> { + HttpServer::new(|| { + let auth = HttpAuthentication::basic(validator); + App::new() + .wrap(middleware::Logger::default()) + .wrap(auth) + .service(web::resource("/").to(|| "Test\r\n")) + }) + .bind("127.0.0.1:8080")? + .workers(1) + .run() +} diff --git a/src/middleware.rs b/src/middleware.rs index e35efdb30..f03f26497 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -1,13 +1,14 @@ //! HTTP Authentication middleware. use std::marker::PhantomData; -use std::rc::Rc; +use std::sync::Arc; use actix_service::{Service, Transform}; use actix_web::dev::{ServiceRequest, ServiceResponse}; use actix_web::Error; use futures::future::{self, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; +use futures_locks::Mutex; use crate::extractors::{basic, bearer, AuthExtractor}; @@ -21,11 +22,12 @@ use crate::extractors::{basic, bearer, AuthExtractor}; /// 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: Rc, + process_fn: Arc, _extractor: PhantomData, } @@ -40,7 +42,7 @@ where /// validation callback `F`. pub fn with_fn(process_fn: F) -> HttpAuthentication { HttpAuthentication { - process_fn: Rc::new(process_fn), + process_fn: Arc::new(process_fn), _extractor: PhantomData, } } @@ -69,7 +71,7 @@ where /// // or to do something else in a async manner. /// fn validator( /// req: ServiceRequest, - /// credentials: BasicAuth, + /// credentials: BasicAuth, /// ) -> FutureResult { /// // All users are great and more than welcome! /// future::ok(req) @@ -140,7 +142,7 @@ where fn new_transform(&self, service: S) -> Self::Future { future::ok(AuthenticationMiddleware { - service: Some(service), + service: Mutex::new(service), process_fn: self.process_fn.clone(), _extractor: PhantomData, }) @@ -152,8 +154,8 @@ pub struct AuthenticationMiddleware where T: AuthExtractor, { - service: Option, - process_fn: Rc, + service: Mutex, + process_fn: Arc, _extractor: PhantomData, } @@ -176,23 +178,24 @@ where fn poll_ready(&mut self) -> Poll<(), Self::Error> { self.service - .as_mut() + .try_lock() .expect("AuthenticationMiddleware was called already") .poll_ready() } fn call(&mut self, req: Self::Request) -> Self::Future { let process_fn = self.process_fn.clone(); - let mut service = self - .service - .take() - .expect("AuthenticationMiddleware was called twice"); + // Note: cloning the mutex, not the service itself + let inner = self.service.clone(); let f = Extract::new(req) - .and_then(move |(req, credentials)| { - (process_fn)(req, credentials) - }) - .and_then(move |req| service.call(req)); + .and_then(move |(req, credentials)| (process_fn)(req, credentials)) + .and_then(move |req| { + inner + .lock() + .map_err(Into::into) + .and_then(|mut service| service.call(req)) + }); Box::new(f) } diff --git a/src/utils.rs b/src/utils.rs index 4312663e1..2b5c6ac31 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -26,24 +26,20 @@ impl<'a> Iterator for Quoted<'a> { 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::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, + State::YieldQuote => match self.inner.peek() { + Some(_) => { + self.state = State::YieldStr; + Some("\\\"") } - } + None => None, + }, } } } From 4eeb1d95998429a3e2ffe123926763145a9b61a0 Mon Sep 17 00:00:00 2001 From: Masaki Hara Date: Sat, 20 Jul 2019 00:34:59 +0900 Subject: [PATCH 26/36] Tighten HttpAuthentication::with_fn bound from FnMut to Fn. (#11) * Tighten HttpAuthentication::with_fn bound from FnMut to Fn. * Add middleware with closure example. --- examples/middleware-closure.rs | 18 ++++++++++++++++++ src/middleware.rs | 6 +++--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 examples/middleware-closure.rs diff --git a/examples/middleware-closure.rs b/examples/middleware-closure.rs new file mode 100644 index 000000000..0135bac9a --- /dev/null +++ b/examples/middleware-closure.rs @@ -0,0 +1,18 @@ +use actix_web::{middleware, web, App, HttpServer}; + +use futures::future; + +use actix_web_httpauth::middleware::HttpAuthentication; + +fn main() -> std::io::Result<()> { + HttpServer::new(|| { + let auth = HttpAuthentication::basic(|req, _credentials| future::ok(req)); + App::new() + .wrap(middleware::Logger::default()) + .wrap(auth) + .service(web::resource("/").to(|| "Test\r\n")) + }) + .bind("127.0.0.1:8080")? + .workers(1) + .run() +} diff --git a/src/middleware.rs b/src/middleware.rs index f03f26497..d9ce8c563 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -34,7 +34,7 @@ where impl HttpAuthentication where T: AuthExtractor, - F: FnMut(ServiceRequest, T) -> O, + F: Fn(ServiceRequest, T) -> O, O: IntoFuture, { /// Construct `HttpAuthentication` middleware @@ -50,7 +50,7 @@ where impl HttpAuthentication where - F: FnMut(ServiceRequest, basic::BasicAuth) -> O, + F: Fn(ServiceRequest, basic::BasicAuth) -> O, O: IntoFuture, { /// Construct `HttpAuthentication` middleware for the HTTP "Basic" @@ -86,7 +86,7 @@ where impl HttpAuthentication where - F: FnMut(ServiceRequest, bearer::BearerAuth) -> O, + F: Fn(ServiceRequest, bearer::BearerAuth) -> O, O: IntoFuture, { /// Construct `HttpAuthentication` middleware for the HTTP "Bearer" From f808e0680d1ce406ba9a5b8e19cc21dc324d06e9 Mon Sep 17 00:00:00 2001 From: svartalf Date: Fri, 19 Jul 2019 18:43:36 +0300 Subject: [PATCH 27/36] Release 0.3.2 --- CHANGELOG.md | 11 ++++++++++- Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ff6c8cf..1e233fae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,16 @@ 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). -## Unreleased +## [0.3.2] - 2019-07-19 +### Changed + - Middleware accepts any `Fn` as a validator function instead of `FnMut` ([#11](https://github.com/svartalf/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` diff --git a/Cargo.toml b/Cargo.toml index 1f48b6bfc..39ba959ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.3.1" +version = "0.3.2" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" From d83cbc8542e068ce3908e070b56895bfe46a2d27 Mon Sep 17 00:00:00 2001 From: Masaki Hara Date: Tue, 7 Jan 2020 01:00:43 +0900 Subject: [PATCH 28/36] Support actix-web 2.x (#14) --- Cargo.toml | 10 ++- examples/middleware-closure.rs | 11 +-- examples/middleware.rs | 14 +-- src/extractors/basic.rs | 57 +++++++------ src/extractors/bearer.rs | 49 ++++++----- src/extractors/mod.rs | 4 +- src/headers/authorization/scheme/basic.rs | 6 +- src/headers/authorization/scheme/bearer.rs | 6 +- .../www_authenticate/challenge/basic.rs | 6 +- .../challenge/bearer/challenge.rs | 6 +- src/middleware.rs | 85 +++++++++---------- 11 files changed, 133 insertions(+), 121 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 39ba959ef..de6c12ae7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,15 @@ exclude = [".travis.yml", ".gitignore"] edition = "2018" [dependencies] -actix-web = { version = "^1.0", default_features = false } -actix-service = "0.4.0" -futures = "0.1" -futures-locks = "0.3.3" +actix-web = { version = "^2.0", default_features = false } +actix-service = "1.0" +futures = "0.3" bytes = "0.4" base64 = "0.10" +[dev-dependencies] +actix-rt = "1.0" + [features] default = [] nightly = [] diff --git a/examples/middleware-closure.rs b/examples/middleware-closure.rs index 0135bac9a..0f623bfe0 100644 --- a/examples/middleware-closure.rs +++ b/examples/middleware-closure.rs @@ -1,18 +1,19 @@ use actix_web::{middleware, web, App, HttpServer}; -use futures::future; - use actix_web_httpauth::middleware::HttpAuthentication; -fn main() -> std::io::Result<()> { +#[actix_rt::main] +async fn main() -> std::io::Result<()> { HttpServer::new(|| { - let auth = HttpAuthentication::basic(|req, _credentials| future::ok(req)); + let auth = + HttpAuthentication::basic(|req, _credentials| async { Ok(req) }); App::new() .wrap(middleware::Logger::default()) .wrap(auth) - .service(web::resource("/").to(|| "Test\r\n")) + .service(web::resource("/").to(|| async { "Test\r\n" })) }) .bind("127.0.0.1:8080")? .workers(1) .run() + .await } diff --git a/examples/middleware.rs b/examples/middleware.rs index 468d69de9..672e08d90 100644 --- a/examples/middleware.rs +++ b/examples/middleware.rs @@ -1,27 +1,27 @@ use actix_web::dev::ServiceRequest; use actix_web::{middleware, web, App, Error, HttpServer}; -use futures::future; - use actix_web_httpauth::extractors::basic::BasicAuth; use actix_web_httpauth::middleware::HttpAuthentication; -fn validator( +async fn validator( req: ServiceRequest, _credentials: BasicAuth, -) -> future::FutureResult { - future::ok(req) +) -> Result { + Ok(req) } -fn main() -> std::io::Result<()> { +#[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(|| "Test\r\n")) + .service(web::resource("/").to(|| async { "Test\r\n" })) }) .bind("127.0.0.1:8080")? .workers(1) .run() + .await } diff --git a/src/extractors/basic.rs b/src/extractors/basic.rs index 3ad5deb1b..700568267 100644 --- a/src/extractors/basic.rs +++ b/src/extractors/basic.rs @@ -5,6 +5,7 @@ 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; @@ -57,7 +58,7 @@ impl AuthExtractorConfig for Config { /// use actix_web::Result; /// use actix_web_httpauth::extractors::basic::BasicAuth; /// -/// fn index(auth: BasicAuth) -> String { +/// async fn index(auth: BasicAuth) -> String { /// format!("Hello, {}!", auth.user_id()) /// } /// ``` @@ -72,7 +73,7 @@ impl AuthExtractorConfig for Config { /// use actix_web::{web, App}; /// use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; /// -/// fn index(auth: BasicAuth) -> String { +/// async fn index(auth: BasicAuth) -> String { /// format!("Hello, {}!", auth.user_id()) /// } /// @@ -101,7 +102,7 @@ impl BasicAuth { } impl FromRequest for BasicAuth { - type Future = Result; + type Future = future::Ready>; type Config = Config; type Error = AuthenticationError; @@ -109,37 +110,41 @@ impl FromRequest for BasicAuth { req: &HttpRequest, _: &mut Payload, ) -> ::Future { - 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); + 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) - }) + AuthenticationError::new(challenge) + }), + ) } } impl AuthExtractor for BasicAuth { type Error = AuthenticationError; - type Future = Result; + type Future = future::Ready>; fn from_service_request(req: &ServiceRequest) -> Self::Future { - 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); + 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) - }) + AuthenticationError::new(challenge) + }), + ) } } diff --git a/src/extractors/bearer.rs b/src/extractors/bearer.rs index a735954c8..37788d70f 100644 --- a/src/extractors/bearer.rs +++ b/src/extractors/bearer.rs @@ -6,6 +6,7 @@ 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; @@ -60,7 +61,7 @@ impl AuthExtractorConfig for Config { /// ```rust /// use actix_web_httpauth::extractors::bearer::BearerAuth; /// -/// fn index(auth: BearerAuth) -> String { +/// async fn index(auth: BearerAuth) -> String { /// format!("Hello, user with token {}!", auth.token()) /// } /// ``` @@ -75,7 +76,7 @@ impl AuthExtractorConfig for Config { /// use actix_web::{web, App}; /// use actix_web_httpauth::extractors::bearer::{BearerAuth, Config}; /// -/// fn index(auth: BearerAuth) -> String { +/// async fn index(auth: BearerAuth) -> String { /// format!("Hello, {}!", auth.token()) /// } /// @@ -101,41 +102,45 @@ impl BearerAuth { impl FromRequest for BearerAuth { type Config = Config; - type Future = Result; + type Future = future::Ready>; type Error = AuthenticationError; fn from_request( req: &HttpRequest, _payload: &mut Payload, ) -> ::Future { - 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); + 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) - }) + AuthenticationError::new(bearer) + }), + ) } } impl AuthExtractor for BearerAuth { - type Future = Result; + type Future = future::Ready>; type Error = AuthenticationError; fn from_service_request(req: &ServiceRequest) -> Self::Future { - 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); + 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) - }) + AuthenticationError::new(bearer) + }), + ) } } diff --git a/src/extractors/mod.rs b/src/extractors/mod.rs index 062efba2d..27a3e4e33 100644 --- a/src/extractors/mod.rs +++ b/src/extractors/mod.rs @@ -2,7 +2,7 @@ use actix_web::dev::ServiceRequest; use actix_web::Error; -use futures::IntoFuture; +use futures::future::Future; pub mod basic; pub mod bearer; @@ -26,7 +26,7 @@ pub trait AuthExtractor: Sized { type Error: Into; /// Future that resolves into extracted credentials type. - type Future: IntoFuture; + type Future: Future>; /// Parse the authentication credentials from the actix' `ServiceRequest`. fn from_service_request(req: &ServiceRequest) -> Self::Future; diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 0889044b3..751c08861 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -3,7 +3,7 @@ use std::fmt; use std::str; use actix_web::http::header::{ - HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, + HeaderValue, IntoHeaderValue, InvalidHeaderValue, }; use base64; use bytes::{BufMut, BytesMut}; @@ -101,7 +101,7 @@ impl fmt::Display for Basic { } impl IntoHeaderValue for Basic { - type Error = InvalidHeaderValueBytes; + type Error = InvalidHeaderValue; fn try_into(self) -> Result::Error> { let mut credentials = BytesMut::with_capacity( @@ -123,7 +123,7 @@ impl IntoHeaderValue for Basic { value.put("Basic "); value.put(&encoded); - HeaderValue::from_shared(value.freeze()) + HeaderValue::from_maybe_shared(value.freeze()) } } diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs index d9eac636d..c163fbdda 100644 --- a/src/headers/authorization/scheme/bearer.rs +++ b/src/headers/authorization/scheme/bearer.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::fmt; use actix_web::http::header::{ - HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, + HeaderValue, IntoHeaderValue, InvalidHeaderValue, }; use bytes::{BufMut, BytesMut}; @@ -76,14 +76,14 @@ impl fmt::Display for Bearer { } impl IntoHeaderValue for Bearer { - type Error = InvalidHeaderValueBytes; + type Error = InvalidHeaderValue; fn try_into(self) -> Result::Error> { let mut buffer = BytesMut::with_capacity(7 + self.token.len()); buffer.put("Bearer "); buffer.extend_from_slice(self.token.as_bytes()); - HeaderValue::from_shared(buffer.freeze()) + HeaderValue::from_maybe_shared(buffer.freeze()) } } diff --git a/src/headers/www_authenticate/challenge/basic.rs b/src/headers/www_authenticate/challenge/basic.rs index d5699e01a..b4cfa3d7a 100644 --- a/src/headers/www_authenticate/challenge/basic.rs +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -6,7 +6,7 @@ use std::fmt; use std::str; use actix_web::http::header::{ - HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, + HeaderValue, IntoHeaderValue, InvalidHeaderValue, }; use bytes::{BufMut, Bytes, BytesMut}; @@ -106,10 +106,10 @@ impl fmt::Display for Basic { } impl IntoHeaderValue for Basic { - type Error = InvalidHeaderValueBytes; + type Error = InvalidHeaderValue; fn try_into(self) -> Result::Error> { - HeaderValue::from_shared(self.to_bytes()) + HeaderValue::from_maybe_shared(self.to_bytes()) } } diff --git a/src/headers/www_authenticate/challenge/bearer/challenge.rs b/src/headers/www_authenticate/challenge/bearer/challenge.rs index 92f977dda..b6f85c1de 100644 --- a/src/headers/www_authenticate/challenge/bearer/challenge.rs +++ b/src/headers/www_authenticate/challenge/bearer/challenge.rs @@ -3,7 +3,7 @@ use std::fmt; use std::str; use actix_web::http::header::{ - HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, + HeaderValue, IntoHeaderValue, InvalidHeaderValue, }; use bytes::{BufMut, Bytes, BytesMut}; @@ -133,9 +133,9 @@ impl fmt::Display for Bearer { } impl IntoHeaderValue for Bearer { - type Error = InvalidHeaderValueBytes; + type Error = InvalidHeaderValue; fn try_into(self) -> Result::Error> { - HeaderValue::from_shared(self.to_bytes()) + HeaderValue::from_maybe_shared(self.to_bytes()) } } diff --git a/src/middleware.rs b/src/middleware.rs index d9ce8c563..3ef8a9018 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -1,14 +1,15 @@ //! 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, FutureResult}; -use futures::{Async, Future, IntoFuture, Poll}; -use futures_locks::Mutex; +use futures::future::{self, Future, FutureExt, LocalBoxFuture, TryFutureExt}; +use futures::lock::Mutex; +use futures::task::{Context, Poll}; use crate::extractors::{basic, bearer, AuthExtractor}; @@ -35,7 +36,7 @@ impl HttpAuthentication where T: AuthExtractor, F: Fn(ServiceRequest, T) -> O, - O: IntoFuture, + O: Future>, { /// Construct `HttpAuthentication` middleware /// with the provided auth extractor `T` and @@ -51,7 +52,7 @@ where impl HttpAuthentication where F: Fn(ServiceRequest, basic::BasicAuth) -> O, - O: IntoFuture, + O: Future>, { /// Construct `HttpAuthentication` middleware for the HTTP "Basic" /// authentication scheme. @@ -61,7 +62,6 @@ where /// ```rust /// # use actix_web::Error; /// # use actix_web::dev::ServiceRequest; - /// # use futures::future::{self, FutureResult}; /// # use actix_web_httpauth::middleware::HttpAuthentication; /// # use actix_web_httpauth::extractors::basic::BasicAuth; /// // In this example validator returns immediately, @@ -69,12 +69,12 @@ where /// // that implements `IntoFuture` trait, /// // it can be extended to query database /// // or to do something else in a async manner. - /// fn validator( + /// async fn validator( /// req: ServiceRequest, /// credentials: BasicAuth, - /// ) -> FutureResult { + /// ) -> Result { /// // All users are great and more than welcome! - /// future::ok(req) + /// Ok(req) /// } /// /// let middleware = HttpAuthentication::basic(validator); @@ -87,7 +87,7 @@ where impl HttpAuthentication where F: Fn(ServiceRequest, bearer::BearerAuth) -> O, - O: IntoFuture, + O: Future>, { /// Construct `HttpAuthentication` middleware for the HTTP "Bearer" /// authentication scheme. @@ -97,20 +97,19 @@ where /// ```rust /// # use actix_web::Error; /// # use actix_web::dev::ServiceRequest; - /// # use futures::future::{self, FutureResult}; /// # use actix_web_httpauth::middleware::HttpAuthentication; /// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth}; /// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig}; - /// fn validator(req: ServiceRequest, credentials: BearerAuth) -> FutureResult { + /// async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result { /// if credentials.token() == "mF_9.B5f-4.1JqM" { - /// future::ok(req) + /// 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"); /// - /// future::err(AuthenticationError::from(config).into()) + /// Err(AuthenticationError::from(config).into()) /// } /// } /// @@ -130,7 +129,7 @@ where > + 'static, S::Future: 'static, F: Fn(ServiceRequest, T) -> O + 'static, - O: IntoFuture + 'static, + O: Future> + 'static, T: AuthExtractor + 'static, { type Request = ServiceRequest; @@ -138,11 +137,11 @@ where type Error = Error; type Transform = AuthenticationMiddleware; type InitError = (); - type Future = FutureResult; + type Future = future::Ready>; fn new_transform(&self, service: S) -> Self::Future { future::ok(AuthenticationMiddleware { - service: Mutex::new(service), + service: Arc::new(Mutex::new(service)), process_fn: self.process_fn.clone(), _extractor: PhantomData, }) @@ -154,7 +153,7 @@ pub struct AuthenticationMiddleware where T: AuthExtractor, { - service: Mutex, + service: Arc>, process_fn: Arc, _extractor: PhantomData, } @@ -168,19 +167,22 @@ where > + 'static, S::Future: 'static, F: Fn(ServiceRequest, T) -> O + 'static, - O: IntoFuture + 'static, + O: Future> + 'static, T: AuthExtractor + 'static, { type Request = ServiceRequest; type Response = ServiceResponse; type Error = S::Error; - type Future = Box, Error = Error>>; + type Future = LocalBoxFuture<'static, Result, Error>>; - fn poll_ready(&mut self) -> Poll<(), Self::Error> { + fn poll_ready( + &mut self, + ctx: &mut Context<'_>, + ) -> Poll> { self.service .try_lock() .expect("AuthenticationMiddleware was called already") - .poll_ready() + .poll_ready(ctx) } fn call(&mut self, req: Self::Request) -> Self::Future { @@ -188,23 +190,20 @@ where // Note: cloning the mutex, not the service itself let inner = self.service.clone(); - let f = Extract::new(req) - .and_then(move |(req, credentials)| (process_fn)(req, credentials)) - .and_then(move |req| { - inner - .lock() - .map_err(Into::into) - .and_then(|mut service| service.call(req)) - }); - - Box::new(f) + 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, + f: Option>>, + _extractor: PhantomData T>, } impl Extract { @@ -223,26 +222,26 @@ where T::Future: 'static, T::Error: 'static, { - type Item = (ServiceRequest, T); - type Error = Error; + type Output = Result<(ServiceRequest, T), Error>; - fn poll(&mut self) -> Poll { + 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) - .into_future() - .map_err(Into::into); - self.f = Some(Box::new(f)); + 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::try_ready!(f.poll()); + let credentials = futures::ready!(Future::poll(f.as_mut(), ctx))?; let req = self.req.take().expect("Extract future was polled twice!"); - Ok(Async::Ready((req, credentials))) + Poll::Ready(Ok((req, credentials))) } } From deb1508d2fe5082a685e7bf617de13c88bc1e2cd Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 7 Jan 2020 13:54:46 +0900 Subject: [PATCH 29/36] Cleanup documentation and metadata (#15) --- .editorconfig | 3 +++ .github/FUNDING.yml | 2 -- CHANGELOG.md | 4 ++-- Cargo.toml | 8 ++++---- LICENSE-APACHE | 2 +- LICENSE-MIT | 2 +- README.md | 8 ++------ 7 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.editorconfig b/.editorconfig index f6da06d69..74c73a970 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,6 @@ charset = utf-8 indent_style = space indent_size = 4 trim_trailing_whitespace = true + +[*.yml] +indent_size = 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index e4510bce2..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: svartalf -custom: https://svartalf.info/donate/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e233fae3..6ea50a810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [0.3.2] - 2019-07-19 ### Changed - - Middleware accepts any `Fn` as a validator function instead of `FnMut` ([#11](https://github.com/svartalf/actix-web-httpauth/pull/11)) + - 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 @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [0.2.0] - 2019-04-26 ### Changed - - `actix-web` dependency is used without default features now ([#6](https://github.com/svartalf/actix-web-httpauth/pull/6)) + - `actix-web` dependency is used without default features now ([#6](https://github.com/actix/actix-web-httpauth/pull/6)) - `base64` dependency version was bumped to `0.10` ## [0.1.0] - 2018-09-08 diff --git a/Cargo.toml b/Cargo.toml index de6c12ae7..10858ca48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "actix-web-httpauth" version = "0.3.2" -authors = ["svartalf "] +authors = ["svartalf ", "Yuki Okushi "] description = "HTTP authentication schemes for actix-web" readme = "README.md" keywords = ["http", "web", "framework"] -homepage = "https://github.com/svartalf/actix-web-httpauth" -repository = "https://github.com/svartalf/actix-web-httpauth.git" +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" @@ -28,5 +28,5 @@ default = [] nightly = [] [badges] -travis-ci = { repository = "svartalf/actix-web-httpauth", branch = "master" } +travis-ci = { repository = "actix/actix-web-httpauth", branch = "master" } maintenance = { status = "passively-maintained" } diff --git a/LICENSE-APACHE b/LICENSE-APACHE index b3844ab27..7692e1a23 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2017-NOW svartalf + 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. diff --git a/LICENSE-MIT b/LICENSE-MIT index a82012880..44a057e4a 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (c) 2017 svartalf +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 diff --git a/README.md b/README.md index 5e5b76d30..a2e9d66b9 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![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.3.1/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.3.1) -![Build Status](https://travis-ci.org/svartalf/actix-web-httpauth.svg?branch=master) +[![dependency status](https://deps.rs/crate/actix-web-httpauth/0.3.2/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.3.2) +![Build Status](https://travis-ci.org/actix/actix-web-httpauth.svg?branch=master) ![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. @@ -20,7 +20,3 @@ and can be used both in the middlewares and request handlers. * [Basic](https://tools.ietf.org/html/rfc7617) * [Bearer](https://tools.ietf.org/html/rfc6750) - -## Donations - -If you appreciate my work and want to support me, you can do it [here](https://svartalf.info/donate/). From b4508baf7e4202538290ebb8ef3720cd83672514 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 7 Jan 2020 18:08:00 +0900 Subject: [PATCH 30/36] Run CI with GitHub Actions (#16) * Run CI with GitHub Actions * Remove benchmarks for now * Check MSRV --- .github/workflows/clippy-and-fmt.yml | 28 ++++++++++ .github/workflows/main.yml | 62 +++++++++++++++++++++++ .github/workflows/msrv.yml | 35 +++++++++++++ src/headers/authorization/scheme/basic.rs | 28 ---------- 4 files changed, 125 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/clippy-and-fmt.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/msrv.yml diff --git a/.github/workflows/clippy-and-fmt.yml b/.github/workflows/clippy-and-fmt.yml new file mode 100644 index 000000000..3ff6c230c --- /dev/null +++ b/.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/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..00b6a510b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,62 @@ +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 }} + default: true + + - 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/.github/workflows/msrv.yml b/.github/workflows/msrv.yml new file mode 100644 index 000000000..f96292e69 --- /dev/null +++ b/.github/workflows/msrv.yml @@ -0,0 +1,35 @@ +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 + default: true + + - 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/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 751c08861..700dd6b24 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -202,31 +202,3 @@ mod tests { ); } } - -#[cfg(all(test, feature = "nightly"))] -mod benches { - use test::Bencher; - - use actix_web::http::header::{HeaderValue, IntoHeaderValue}; - - use super::{Basic, Scheme}; - - #[bench] - fn bench_parsing(b: &mut Bencher) { - let value = - HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); - b.iter(|| Basic::parse(&value)); - } - - #[bench] - fn bench_serializing(b: &mut Bencher) { - b.iter(|| { - let basic = Basic { - user_id: "Aladdin".into(), - password: Some("open sesame".into()), - }; - - basic.try_into() - }) - } -} From cfaa307d475e2628a5b54be53f2865133660644b Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Thu, 9 Jan 2020 17:51:53 +0900 Subject: [PATCH 31/36] Cache build to decrease CI time (#17) --- .github/workflows/main.yml | 23 ++++++++++++++++++++++- .github/workflows/msrv.yml | 22 +++++++++++++++++++++- .travis.yml | 6 ------ 3 files changed, 43 insertions(+), 8 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 00b6a510b..071b73855 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,7 +39,28 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.version }}-${{ matrix.toolchain }} - default: true + 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 diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml index f96292e69..68f1fcecc 100644 --- a/.github/workflows/msrv.yml +++ b/.github/workflows/msrv.yml @@ -20,7 +20,27 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: 1.39.0-x86_64-unknown-linux-gnu - default: true + 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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d2da893a0..000000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: rust -cache: cargo -rust: - - stable - - beta - - nightly From 4d9fb6825c2b16942026a9d7888587c262c7e8c9 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Fri, 10 Jan 2020 05:57:42 +0900 Subject: [PATCH 32/36] Update bytes to 0.5 and base64 to 0.11 (#18) * Update bytes to 0.5 and base64 to 0.11 * Update changelog --- .editorconfig | 3 ++ CHANGELOG.md | 38 +++++++++++++------ Cargo.toml | 4 +- src/headers/authorization/scheme/basic.rs | 4 +- src/headers/authorization/scheme/bearer.rs | 2 +- .../www_authenticate/challenge/basic.rs | 6 +-- .../challenge/bearer/challenge.rs | 22 +++++------ 7 files changed, 48 insertions(+), 31 deletions(-) diff --git a/.editorconfig b/.editorconfig index 74c73a970..dc6e34dda 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,6 @@ trim_trailing_whitespace = true [*.yml] indent_size = 2 + +[*.md] +indent_size = 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ea50a810..0522c1bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,31 +5,45 @@ 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.3.2] - 2019-07-19 +## [0.4.0] - 2020-01 + ### Changed - - Middleware accepts any `Fn` as a validator function instead of `FnMut` ([#11](https://github.com/actix/actix-web-httpauth/pull/11)) + - 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 + - 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 + - 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` + - `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 + - 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 + - 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/Cargo.toml b/Cargo.toml index 10858ca48..0d92f75de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,8 @@ edition = "2018" actix-web = { version = "^2.0", default_features = false } actix-service = "1.0" futures = "0.3" -bytes = "0.4" -base64 = "0.10" +bytes = "0.5" +base64 = "0.11" [dev-dependencies] actix-rt = "1.0" diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 700dd6b24..0cea7e18b 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -120,8 +120,8 @@ impl IntoHeaderValue for Basic { // directly to `value` let encoded = base64::encode(&credentials); let mut value = BytesMut::with_capacity(6 + encoded.len()); - value.put("Basic "); - value.put(&encoded); + value.put(&b"Basic "[..]); + value.put(&encoded.as_bytes()[..]); HeaderValue::from_maybe_shared(value.freeze()) } diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs index c163fbdda..a1bd3434f 100644 --- a/src/headers/authorization/scheme/bearer.rs +++ b/src/headers/authorization/scheme/bearer.rs @@ -80,7 +80,7 @@ impl IntoHeaderValue for Bearer { fn try_into(self) -> Result::Error> { let mut buffer = BytesMut::with_capacity(7 + self.token.len()); - buffer.put("Bearer "); + buffer.put(&b"Bearer "[..]); buffer.extend_from_slice(self.token.as_bytes()); HeaderValue::from_maybe_shared(buffer.freeze()) diff --git a/src/headers/www_authenticate/challenge/basic.rs b/src/headers/www_authenticate/challenge/basic.rs index b4cfa3d7a..a1d066590 100644 --- a/src/headers/www_authenticate/challenge/basic.rs +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -82,11 +82,11 @@ impl Challenge for Basic { // 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("Basic"); + buffer.put(&b"Basic"[..]); if let Some(ref realm) = self.realm { - buffer.put(" realm=\""); + buffer.put(&b" realm=\""[..]); utils::put_quoted(&mut buffer, realm); - buffer.put("\""); + buffer.put_u8(b'"'); } buffer.freeze() diff --git a/src/headers/www_authenticate/challenge/bearer/challenge.rs b/src/headers/www_authenticate/challenge/bearer/challenge.rs index b6f85c1de..d30883263 100644 --- a/src/headers/www_authenticate/challenge/bearer/challenge.rs +++ b/src/headers/www_authenticate/challenge/bearer/challenge.rs @@ -78,18 +78,18 @@ impl Challenge for Bearer { + self.scope.as_ref().map_or(0, |scope| scope.len() + 9) + desc_uri_required; let mut buffer = BytesMut::with_capacity(capacity); - buffer.put("Bearer"); + buffer.put(&b"Bearer"[..]); if let Some(ref realm) = self.realm { - buffer.put(" realm=\""); + buffer.put(&b" realm=\""[..]); utils::put_quoted(&mut buffer, realm); - buffer.put("\""); + buffer.put_u8(b'"'); } if let Some(ref scope) = self.scope { - buffer.put(" scope=\""); + buffer.put(&b" scope=\""[..]); utils::put_quoted(&mut buffer, scope); - buffer.put("\""); + buffer.put_u8(b'"'); } if let Some(ref error) = self.error { @@ -99,21 +99,21 @@ impl Challenge for Bearer { if remaining < required { buffer.reserve(required); } - buffer.put(" error=\""); + buffer.put(&b" error=\""[..]); utils::put_quoted(&mut buffer, error_repr); - buffer.put("\"") + buffer.put_u8(b'"') } if let Some(ref error_description) = self.error_description { - buffer.put(" error_description=\""); + buffer.put(&b" error_description=\""[..]); utils::put_quoted(&mut buffer, error_description); - buffer.put("\""); + buffer.put_u8(b'"'); } if let Some(ref error_uri) = self.error_uri { - buffer.put(" error_uri=\""); + buffer.put(&b" error_uri=\""[..]); utils::put_quoted(&mut buffer, error_uri); - buffer.put("\""); + buffer.put_u8(b'"'); } buffer.freeze() From d757f445576759c22c8012714441f15524b9cb2a Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 11 Jan 2020 22:12:11 +0900 Subject: [PATCH 33/36] Remove `description()` (#19) * Remove `description()` It has been deprecated. --- src/headers/authorization/errors.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/headers/authorization/errors.rs b/src/headers/authorization/errors.rs index 6ba240507..4de1b72ce 100644 --- a/src/headers/authorization/errors.rs +++ b/src/headers/authorization/errors.rs @@ -27,22 +27,21 @@ pub enum ParseError { impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.description()) + 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 description(&self) -> &str { - match self { - ParseError::Invalid => "Invalid header value", - ParseError::MissingScheme => "Missing authorization scheme", - ParseError::MissingField(_) => "Missing header field", - ParseError::ToStrError(e) => e.description(), - ParseError::Base64DecodeError(e) => e.description(), - ParseError::Utf8Error(e) => e.description(), - } - } - fn source(&self) -> Option<&(dyn Error + 'static)> { match self { ParseError::Invalid => None, From 8dd2c7716551bf9d9346b8dda830dfcf8bc7af67 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 14 Jan 2020 13:31:20 +0900 Subject: [PATCH 34/36] Make the lint stricter (#20) * Make the lint stricter * Add explicit lifetimes * Allow `needless_doctest_main` --- src/extractors/basic.rs | 6 ++++-- src/extractors/bearer.rs | 6 ++++-- src/extractors/errors.rs | 2 +- src/headers/authorization/errors.rs | 2 +- src/headers/authorization/header.rs | 4 ++-- src/headers/authorization/scheme/basic.rs | 6 +++--- src/headers/authorization/scheme/bearer.rs | 6 +++--- src/headers/www_authenticate/challenge/basic.rs | 10 +++++----- .../www_authenticate/challenge/bearer/challenge.rs | 6 +++--- .../www_authenticate/challenge/bearer/errors.rs | 2 +- src/lib.rs | 3 +++ src/middleware.rs | 4 ++-- src/utils.rs | 2 +- 13 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/extractors/basic.rs b/src/extractors/basic.rs index 700568267..467be7f02 100644 --- a/src/extractors/basic.rs +++ b/src/extractors/basic.rs @@ -50,11 +50,13 @@ impl AuthExtractorConfig for Config { } } +// Needs `fn main` to display complete example. +#[allow(clippy::needless_doctest_main)] /// Extractor for HTTP Basic auth. /// /// # Example /// -/// ```rust +/// ``` /// use actix_web::Result; /// use actix_web_httpauth::extractors::basic::BasicAuth; /// @@ -69,7 +71,7 @@ impl AuthExtractorConfig for Config { /// /// ## Example /// -/// ```rust +/// ``` /// use actix_web::{web, App}; /// use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; /// diff --git a/src/extractors/bearer.rs b/src/extractors/bearer.rs index 37788d70f..566e77b8d 100644 --- a/src/extractors/bearer.rs +++ b/src/extractors/bearer.rs @@ -54,11 +54,13 @@ impl AuthExtractorConfig for Config { } } +// Needs `fn main` to display complete example. +#[allow(clippy::needless_doctest_main)] /// Extractor for HTTP Bearer auth /// /// # Example /// -/// ```rust +/// ``` /// use actix_web_httpauth::extractors::bearer::BearerAuth; /// /// async fn index(auth: BearerAuth) -> String { @@ -72,7 +74,7 @@ impl AuthExtractorConfig for Config { /// /// ## Example /// -/// ```rust +/// ``` /// use actix_web::{web, App}; /// use actix_web_httpauth::extractors::bearer::{BearerAuth, Config}; /// diff --git a/src/extractors/errors.rs b/src/extractors/errors.rs index 1834f6b8a..c136d6617 100644 --- a/src/extractors/errors.rs +++ b/src/extractors/errors.rs @@ -43,7 +43,7 @@ impl AuthenticationError { } impl fmt::Display for AuthenticationError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.status_code, f) } } diff --git a/src/headers/authorization/errors.rs b/src/headers/authorization/errors.rs index 4de1b72ce..f2c620063 100644 --- a/src/headers/authorization/errors.rs +++ b/src/headers/authorization/errors.rs @@ -26,7 +26,7 @@ pub enum ParseError { } impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let display = match self { ParseError::Invalid => "Invalid header value".to_string(), ParseError::MissingScheme => { diff --git a/src/headers/authorization/header.rs b/src/headers/authorization/header.rs index d6f051124..3fb9531d2 100644 --- a/src/headers/authorization/header.rs +++ b/src/headers/authorization/header.rs @@ -21,7 +21,7 @@ use crate::headers::authorization::scheme::Scheme; /// /// # Example /// -/// ```rust +/// ``` /// # use actix_web::http::header::Header; /// # use actix_web::{HttpRequest, Result}; /// # use actix_web_httpauth::headers::authorization::{Authorization, Basic}; @@ -98,7 +98,7 @@ impl IntoHeaderValue for Authorization { } impl fmt::Display for Authorization { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 0cea7e18b..107217ba3 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -24,7 +24,7 @@ impl Basic { /// /// ## Example /// - /// ```rust + /// ``` /// # use actix_web_httpauth::headers::authorization::Basic; /// let credentials = Basic::new("Alladin", Some("open sesame")); /// ``` @@ -89,13 +89,13 @@ impl Scheme for Basic { } impl fmt::Debug for Basic { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_fmt(format_args!("Basic {}:******", self.user_id)) } } impl fmt::Display for Basic { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_fmt(format_args!("Basic {}:******", self.user_id)) } } diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs index a1bd3434f..ed52ee1b5 100644 --- a/src/headers/authorization/scheme/bearer.rs +++ b/src/headers/authorization/scheme/bearer.rs @@ -23,7 +23,7 @@ impl Bearer { /// /// ## Example /// - /// ```rust + /// ``` /// # use actix_web_httpauth::headers::authorization::Bearer; /// let credentials = Bearer::new("mF_9.B5f-4.1JqM"); /// ``` @@ -64,13 +64,13 @@ impl Scheme for Bearer { } impl fmt::Debug for Bearer { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + 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 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_fmt(format_args!("Bearer {}", self.token)) } } diff --git a/src/headers/www_authenticate/challenge/basic.rs b/src/headers/www_authenticate/challenge/basic.rs index a1d066590..1a01c965d 100644 --- a/src/headers/www_authenticate/challenge/basic.rs +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -18,7 +18,7 @@ use crate::utils; /// /// ## Example /// -/// ```rust +/// ``` /// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; /// use actix_web_httpauth::headers::www_authenticate::basic::Basic; /// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate; @@ -44,7 +44,7 @@ impl Basic { /// /// ## Example /// - /// ```rust + /// ``` /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; /// let challenge = Basic::new(); /// ``` @@ -56,12 +56,12 @@ impl Basic { /// /// ## Examples /// - /// ```rust + /// ``` /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; /// let challenge = Basic::with_realm("Restricted area"); /// ``` /// - /// ```rust + /// ``` /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; /// let my_realm = "Earth realm".to_string(); /// let challenge = Basic::with_realm(my_realm); @@ -94,7 +94,7 @@ impl Challenge for Basic { } impl fmt::Display for Basic { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + 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 diff --git a/src/headers/www_authenticate/challenge/bearer/challenge.rs b/src/headers/www_authenticate/challenge/bearer/challenge.rs index d30883263..9207b59d0 100644 --- a/src/headers/www_authenticate/challenge/bearer/challenge.rs +++ b/src/headers/www_authenticate/challenge/bearer/challenge.rs @@ -16,7 +16,7 @@ use crate::utils; /// /// ## Example /// -/// ```rust +/// ``` /// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; /// use actix_web_httpauth::headers::www_authenticate::bearer::{ /// Bearer, Error, @@ -53,7 +53,7 @@ impl Bearer { /// /// ## Example /// - /// ```rust + /// ``` /// # use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer}; /// let challenge = Bearer::build() /// .realm("Restricted area") @@ -121,7 +121,7 @@ impl Challenge for Bearer { } impl fmt::Display for Bearer { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + 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 diff --git a/src/headers/www_authenticate/challenge/bearer/errors.rs b/src/headers/www_authenticate/challenge/bearer/errors.rs index 7bea8d4a4..fb2f9a3dd 100644 --- a/src/headers/www_authenticate/challenge/bearer/errors.rs +++ b/src/headers/www_authenticate/challenge/bearer/errors.rs @@ -45,7 +45,7 @@ impl Error { } impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } diff --git a/src/lib.rs b/src/lib.rs index 422eace13..2807fa07d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,10 @@ #![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; diff --git a/src/middleware.rs b/src/middleware.rs index 3ef8a9018..f0ce7fc28 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -59,7 +59,7 @@ where /// /// ## Example /// - /// ```rust + /// ``` /// # use actix_web::Error; /// # use actix_web::dev::ServiceRequest; /// # use actix_web_httpauth::middleware::HttpAuthentication; @@ -94,7 +94,7 @@ where /// /// ## Example /// - /// ```rust + /// ``` /// # use actix_web::Error; /// # use actix_web::dev::ServiceRequest; /// # use actix_web_httpauth::middleware::HttpAuthentication; diff --git a/src/utils.rs b/src/utils.rs index 2b5c6ac31..b9296b037 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -13,7 +13,7 @@ struct Quoted<'a> { } impl<'a> Quoted<'a> { - pub fn new(s: &'a str) -> Quoted { + pub fn new(s: &'a str) -> Quoted<'_> { Quoted { inner: s.split('"').peekable(), state: State::YieldStr, From c4b3ef64c21399e418c2936105c86439ccec9504 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Wed, 15 Jan 2020 00:03:23 +0900 Subject: [PATCH 35/36] Prepare release for 0.4.0 (#21) * Prepare release for 0.4.0 * Update CHANGELOG.md --- CHANGELOG.md | 2 +- Cargo.toml | 5 ++--- README.md | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0522c1bfb..8fe17f1f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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 +## [0.4.0] - 2020-01-14 ### Changed - Depends on `actix-web = "^2.0"`, `actix-service = "^1.0"`, and `futures = "^0.3"` version now ([#14]) diff --git a/Cargo.toml b/Cargo.toml index 0d92f75de..8576d509d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.3.2" +version = "0.4.0" authors = ["svartalf ", "Yuki Okushi "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -10,7 +10,7 @@ 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 = [".travis.yml", ".gitignore"] +exclude = [".github/*", ".gitignore"] edition = "2018" [dependencies] @@ -28,5 +28,4 @@ default = [] nightly = [] [badges] -travis-ci = { repository = "actix/actix-web-httpauth", branch = "master" } maintenance = { status = "passively-maintained" } diff --git a/README.md b/README.md index a2e9d66b9..50e9ffab4 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![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.3.2/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.3.2) -![Build Status](https://travis-ci.org/actix/actix-web-httpauth.svg?branch=master) +[![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. From b4267818de8239e2e054a4b5a6315e336ea25efe Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 29 Jan 2020 11:33:00 +0000 Subject: [PATCH 36/36] move files into module --- .editorconfig => actix-web-httpauth/.editorconfig | 0 .../.github}/workflows/clippy-and-fmt.yml | 0 {.github => actix-web-httpauth/.github}/workflows/main.yml | 0 {.github => actix-web-httpauth/.github}/workflows/msrv.yml | 0 .gitignore => actix-web-httpauth/.gitignore | 0 CHANGELOG.md => actix-web-httpauth/CHANGELOG.md | 0 Cargo.toml => actix-web-httpauth/Cargo.toml | 0 LICENSE-APACHE => actix-web-httpauth/LICENSE-APACHE | 0 LICENSE-MIT => actix-web-httpauth/LICENSE-MIT | 0 README.md => actix-web-httpauth/README.md | 0 {examples => actix-web-httpauth/examples}/middleware-closure.rs | 0 {examples => actix-web-httpauth/examples}/middleware.rs | 0 rustfmt.toml => actix-web-httpauth/rustfmt.toml | 0 {src => actix-web-httpauth/src}/extractors/basic.rs | 0 {src => actix-web-httpauth/src}/extractors/bearer.rs | 0 {src => actix-web-httpauth/src}/extractors/config.rs | 0 {src => actix-web-httpauth/src}/extractors/errors.rs | 0 {src => actix-web-httpauth/src}/extractors/mod.rs | 0 {src => actix-web-httpauth/src}/headers/authorization/errors.rs | 0 {src => actix-web-httpauth/src}/headers/authorization/header.rs | 0 {src => actix-web-httpauth/src}/headers/authorization/mod.rs | 0 .../src}/headers/authorization/scheme/basic.rs | 0 .../src}/headers/authorization/scheme/bearer.rs | 0 .../src}/headers/authorization/scheme/mod.rs | 0 {src => actix-web-httpauth/src}/headers/mod.rs | 0 .../src}/headers/www_authenticate/challenge/basic.rs | 0 .../src}/headers/www_authenticate/challenge/bearer/builder.rs | 0 .../src}/headers/www_authenticate/challenge/bearer/challenge.rs | 0 .../src}/headers/www_authenticate/challenge/bearer/errors.rs | 0 .../src}/headers/www_authenticate/challenge/bearer/mod.rs | 0 .../src}/headers/www_authenticate/challenge/bearer/tests.rs | 0 .../src}/headers/www_authenticate/challenge/mod.rs | 0 .../src}/headers/www_authenticate/header.rs | 0 {src => actix-web-httpauth/src}/headers/www_authenticate/mod.rs | 0 {src => actix-web-httpauth/src}/lib.rs | 0 {src => actix-web-httpauth/src}/middleware.rs | 0 {src => actix-web-httpauth/src}/utils.rs | 0 37 files changed, 0 insertions(+), 0 deletions(-) rename .editorconfig => actix-web-httpauth/.editorconfig (100%) rename {.github => actix-web-httpauth/.github}/workflows/clippy-and-fmt.yml (100%) rename {.github => actix-web-httpauth/.github}/workflows/main.yml (100%) rename {.github => actix-web-httpauth/.github}/workflows/msrv.yml (100%) rename .gitignore => actix-web-httpauth/.gitignore (100%) rename CHANGELOG.md => actix-web-httpauth/CHANGELOG.md (100%) rename Cargo.toml => actix-web-httpauth/Cargo.toml (100%) rename LICENSE-APACHE => actix-web-httpauth/LICENSE-APACHE (100%) rename LICENSE-MIT => actix-web-httpauth/LICENSE-MIT (100%) rename README.md => actix-web-httpauth/README.md (100%) rename {examples => actix-web-httpauth/examples}/middleware-closure.rs (100%) rename {examples => actix-web-httpauth/examples}/middleware.rs (100%) rename rustfmt.toml => actix-web-httpauth/rustfmt.toml (100%) rename {src => actix-web-httpauth/src}/extractors/basic.rs (100%) rename {src => actix-web-httpauth/src}/extractors/bearer.rs (100%) rename {src => actix-web-httpauth/src}/extractors/config.rs (100%) rename {src => actix-web-httpauth/src}/extractors/errors.rs (100%) rename {src => actix-web-httpauth/src}/extractors/mod.rs (100%) rename {src => actix-web-httpauth/src}/headers/authorization/errors.rs (100%) rename {src => actix-web-httpauth/src}/headers/authorization/header.rs (100%) rename {src => actix-web-httpauth/src}/headers/authorization/mod.rs (100%) rename {src => actix-web-httpauth/src}/headers/authorization/scheme/basic.rs (100%) rename {src => actix-web-httpauth/src}/headers/authorization/scheme/bearer.rs (100%) rename {src => actix-web-httpauth/src}/headers/authorization/scheme/mod.rs (100%) rename {src => actix-web-httpauth/src}/headers/mod.rs (100%) rename {src => actix-web-httpauth/src}/headers/www_authenticate/challenge/basic.rs (100%) rename {src => actix-web-httpauth/src}/headers/www_authenticate/challenge/bearer/builder.rs (100%) rename {src => actix-web-httpauth/src}/headers/www_authenticate/challenge/bearer/challenge.rs (100%) rename {src => actix-web-httpauth/src}/headers/www_authenticate/challenge/bearer/errors.rs (100%) rename {src => actix-web-httpauth/src}/headers/www_authenticate/challenge/bearer/mod.rs (100%) rename {src => actix-web-httpauth/src}/headers/www_authenticate/challenge/bearer/tests.rs (100%) rename {src => actix-web-httpauth/src}/headers/www_authenticate/challenge/mod.rs (100%) rename {src => actix-web-httpauth/src}/headers/www_authenticate/header.rs (100%) rename {src => actix-web-httpauth/src}/headers/www_authenticate/mod.rs (100%) rename {src => actix-web-httpauth/src}/lib.rs (100%) rename {src => actix-web-httpauth/src}/middleware.rs (100%) rename {src => actix-web-httpauth/src}/utils.rs (100%) diff --git a/.editorconfig b/actix-web-httpauth/.editorconfig similarity index 100% rename from .editorconfig rename to actix-web-httpauth/.editorconfig diff --git a/.github/workflows/clippy-and-fmt.yml b/actix-web-httpauth/.github/workflows/clippy-and-fmt.yml similarity index 100% rename from .github/workflows/clippy-and-fmt.yml rename to actix-web-httpauth/.github/workflows/clippy-and-fmt.yml diff --git a/.github/workflows/main.yml b/actix-web-httpauth/.github/workflows/main.yml similarity index 100% rename from .github/workflows/main.yml rename to actix-web-httpauth/.github/workflows/main.yml diff --git a/.github/workflows/msrv.yml b/actix-web-httpauth/.github/workflows/msrv.yml similarity index 100% rename from .github/workflows/msrv.yml rename to actix-web-httpauth/.github/workflows/msrv.yml diff --git a/.gitignore b/actix-web-httpauth/.gitignore similarity index 100% rename from .gitignore rename to actix-web-httpauth/.gitignore diff --git a/CHANGELOG.md b/actix-web-httpauth/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to actix-web-httpauth/CHANGELOG.md diff --git a/Cargo.toml b/actix-web-httpauth/Cargo.toml similarity index 100% rename from Cargo.toml rename to actix-web-httpauth/Cargo.toml diff --git a/LICENSE-APACHE b/actix-web-httpauth/LICENSE-APACHE similarity index 100% rename from LICENSE-APACHE rename to actix-web-httpauth/LICENSE-APACHE diff --git a/LICENSE-MIT b/actix-web-httpauth/LICENSE-MIT similarity index 100% rename from LICENSE-MIT rename to actix-web-httpauth/LICENSE-MIT diff --git a/README.md b/actix-web-httpauth/README.md similarity index 100% rename from README.md rename to actix-web-httpauth/README.md diff --git a/examples/middleware-closure.rs b/actix-web-httpauth/examples/middleware-closure.rs similarity index 100% rename from examples/middleware-closure.rs rename to actix-web-httpauth/examples/middleware-closure.rs diff --git a/examples/middleware.rs b/actix-web-httpauth/examples/middleware.rs similarity index 100% rename from examples/middleware.rs rename to actix-web-httpauth/examples/middleware.rs diff --git a/rustfmt.toml b/actix-web-httpauth/rustfmt.toml similarity index 100% rename from rustfmt.toml rename to actix-web-httpauth/rustfmt.toml diff --git a/src/extractors/basic.rs b/actix-web-httpauth/src/extractors/basic.rs similarity index 100% rename from src/extractors/basic.rs rename to actix-web-httpauth/src/extractors/basic.rs diff --git a/src/extractors/bearer.rs b/actix-web-httpauth/src/extractors/bearer.rs similarity index 100% rename from src/extractors/bearer.rs rename to actix-web-httpauth/src/extractors/bearer.rs diff --git a/src/extractors/config.rs b/actix-web-httpauth/src/extractors/config.rs similarity index 100% rename from src/extractors/config.rs rename to actix-web-httpauth/src/extractors/config.rs diff --git a/src/extractors/errors.rs b/actix-web-httpauth/src/extractors/errors.rs similarity index 100% rename from src/extractors/errors.rs rename to actix-web-httpauth/src/extractors/errors.rs diff --git a/src/extractors/mod.rs b/actix-web-httpauth/src/extractors/mod.rs similarity index 100% rename from src/extractors/mod.rs rename to actix-web-httpauth/src/extractors/mod.rs diff --git a/src/headers/authorization/errors.rs b/actix-web-httpauth/src/headers/authorization/errors.rs similarity index 100% rename from src/headers/authorization/errors.rs rename to actix-web-httpauth/src/headers/authorization/errors.rs diff --git a/src/headers/authorization/header.rs b/actix-web-httpauth/src/headers/authorization/header.rs similarity index 100% rename from src/headers/authorization/header.rs rename to actix-web-httpauth/src/headers/authorization/header.rs diff --git a/src/headers/authorization/mod.rs b/actix-web-httpauth/src/headers/authorization/mod.rs similarity index 100% rename from src/headers/authorization/mod.rs rename to actix-web-httpauth/src/headers/authorization/mod.rs diff --git a/src/headers/authorization/scheme/basic.rs b/actix-web-httpauth/src/headers/authorization/scheme/basic.rs similarity index 100% rename from src/headers/authorization/scheme/basic.rs rename to actix-web-httpauth/src/headers/authorization/scheme/basic.rs diff --git a/src/headers/authorization/scheme/bearer.rs b/actix-web-httpauth/src/headers/authorization/scheme/bearer.rs similarity index 100% rename from src/headers/authorization/scheme/bearer.rs rename to actix-web-httpauth/src/headers/authorization/scheme/bearer.rs diff --git a/src/headers/authorization/scheme/mod.rs b/actix-web-httpauth/src/headers/authorization/scheme/mod.rs similarity index 100% rename from src/headers/authorization/scheme/mod.rs rename to actix-web-httpauth/src/headers/authorization/scheme/mod.rs diff --git a/src/headers/mod.rs b/actix-web-httpauth/src/headers/mod.rs similarity index 100% rename from src/headers/mod.rs rename to actix-web-httpauth/src/headers/mod.rs diff --git a/src/headers/www_authenticate/challenge/basic.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/basic.rs similarity index 100% rename from src/headers/www_authenticate/challenge/basic.rs rename to actix-web-httpauth/src/headers/www_authenticate/challenge/basic.rs diff --git a/src/headers/www_authenticate/challenge/bearer/builder.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/builder.rs similarity index 100% rename from src/headers/www_authenticate/challenge/bearer/builder.rs rename to actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/builder.rs diff --git a/src/headers/www_authenticate/challenge/bearer/challenge.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/challenge.rs similarity index 100% rename from src/headers/www_authenticate/challenge/bearer/challenge.rs rename to actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/challenge.rs diff --git a/src/headers/www_authenticate/challenge/bearer/errors.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/errors.rs similarity index 100% rename from src/headers/www_authenticate/challenge/bearer/errors.rs rename to actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/errors.rs diff --git a/src/headers/www_authenticate/challenge/bearer/mod.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/mod.rs similarity index 100% rename from src/headers/www_authenticate/challenge/bearer/mod.rs rename to actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/mod.rs diff --git a/src/headers/www_authenticate/challenge/bearer/tests.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/tests.rs similarity index 100% rename from src/headers/www_authenticate/challenge/bearer/tests.rs rename to actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/tests.rs diff --git a/src/headers/www_authenticate/challenge/mod.rs b/actix-web-httpauth/src/headers/www_authenticate/challenge/mod.rs similarity index 100% rename from src/headers/www_authenticate/challenge/mod.rs rename to actix-web-httpauth/src/headers/www_authenticate/challenge/mod.rs diff --git a/src/headers/www_authenticate/header.rs b/actix-web-httpauth/src/headers/www_authenticate/header.rs similarity index 100% rename from src/headers/www_authenticate/header.rs rename to actix-web-httpauth/src/headers/www_authenticate/header.rs diff --git a/src/headers/www_authenticate/mod.rs b/actix-web-httpauth/src/headers/www_authenticate/mod.rs similarity index 100% rename from src/headers/www_authenticate/mod.rs rename to actix-web-httpauth/src/headers/www_authenticate/mod.rs diff --git a/src/lib.rs b/actix-web-httpauth/src/lib.rs similarity index 100% rename from src/lib.rs rename to actix-web-httpauth/src/lib.rs diff --git a/src/middleware.rs b/actix-web-httpauth/src/middleware.rs similarity index 100% rename from src/middleware.rs rename to actix-web-httpauth/src/middleware.rs diff --git a/src/utils.rs b/actix-web-httpauth/src/utils.rs similarity index 100% rename from src/utils.rs rename to actix-web-httpauth/src/utils.rs