From 4baa37f8ad64f1ba183c468db43753fc0597ca7e Mon Sep 17 00:00:00 2001 From: kana-rus Date: Wed, 26 Jun 2024 23:15:32 +0900 Subject: [PATCH 1/2] add `builtin::fang::BasicAuth` --- ohkami/src/builtin/fang.rs | 7 +- ohkami/src/builtin/fang/basicauth.rs | 108 +++++++++++++++++++++++++++ ohkami/src/builtin/fang/jwt.rs | 21 +++++- ohkami/src/response/headers.rs | 5 +- 4 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 ohkami/src/builtin/fang/basicauth.rs diff --git a/ohkami/src/builtin/fang.rs b/ohkami/src/builtin/fang.rs index fa0cb9a9..4bee18d9 100644 --- a/ohkami/src/builtin/fang.rs +++ b/ohkami/src/builtin/fang.rs @@ -1,8 +1,11 @@ +pub(crate) mod jwt; +pub use jwt::JWT; + pub(crate) mod cors; pub use cors::CORS; -pub(crate) mod jwt; -pub use jwt::JWT; +pub(crate) mod basicauth; +pub use basicauth::BasicAuth; #[cfg(any(feature="rt_tokio",feature="rt_async-std"))] pub(crate) mod timeout; diff --git a/ohkami/src/builtin/fang/basicauth.rs b/ohkami/src/builtin/fang/basicauth.rs new file mode 100644 index 00000000..243b0bb0 --- /dev/null +++ b/ohkami/src/builtin/fang/basicauth.rs @@ -0,0 +1,108 @@ +use crate::prelude::*; + + +/// # Builtin fang for Basic Auth +/// +/// - `BasicAuth { username, password }` verifies each request to have the +/// `username` and `password` +/// - `[BasicAuth; N]` verifies each request to have one of the pairs of +/// `username` and `password` +/// +/// *example* +/// ```rust,no_run +/// use ohkami::prelude::*; +/// use ohkami::builtin::fang::BasicAuth; +/// +/// #[tokio::main] +/// async fn main() { +/// Ohkami::new(( +/// "/hello".GET(|| async {"Hello, public!"}), +/// "/private".By(Ohkami::with( +/// BasicAuth { +/// username: "master of hello", +/// password: "world" +/// }, +/// "/hello".GET(|| async {"Hello, private :)"}) +/// )) +/// )).howl("localhost:8888").await +/// } +/// ``` +#[derive(Clone)] +pub struct BasicAuth +where + S: AsRef + Clone + Send + Sync + 'static +{ + pub username: S, + pub password: S +} + +impl BasicAuth +where + S: AsRef + Clone + Send + Sync + 'static +{ + #[inline] + fn matches(&self, + username: &str, + password: &str + ) -> bool { + self.username.as_ref() == username && + self.password.as_ref() == password + } +} + +const _: () = { + fn unauthorized() -> Response { + Response::Unauthorized().with_headers(|h|h + .WWWAuthenticate("Basic realm=\"Secure Area\"") + ) + } + + #[inline] + fn basic_credential_of(req: &Request) -> Result { + let credential_base64 = req.headers + .Authorization().ok_or_else(unauthorized)? + .strip_prefix("Basic ").ok_or_else(unauthorized)?; + + let credential = String::from_utf8( + ohkami_lib::base64::decode(credential_base64.as_bytes()) + ).map_err(|_| unauthorized())?; + + Ok(credential) + } + + impl FangAction for BasicAuth + where + S: AsRef + Clone + Send + Sync + 'static + { + #[inline] + async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { + let credential = basic_credential_of(req)?; + let (username, password) = credential.split_once(':') + .ok_or_else(unauthorized)?; + + self.matches(username, password).then_some(()) + .ok_or_else(unauthorized)?; + + Ok(()) + } + } + + impl FangAction for [BasicAuth; N] + where + S: AsRef + Clone + Send + Sync + 'static + { + #[inline] + async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { + let credential = basic_credential_of(req)?; + let (username, password) = credential.split_once(':') + .ok_or_else(unauthorized)?; + + self.iter() + .map(|candidate| candidate.matches(username, password)) + .any(|matched| matched).then_some(()) + .ok_or_else(unauthorized)?; + + Ok(()) + } + } +}; diff --git a/ohkami/src/builtin/fang/jwt.rs b/ohkami/src/builtin/fang/jwt.rs index 4a166240..bd8ca383 100644 --- a/ohkami/src/builtin/fang/jwt.rs +++ b/ohkami/src/builtin/fang/jwt.rs @@ -10,6 +10,25 @@ use crate::{Fang, FangProc, IntoResponse, Request, Response}; /// ///
/// +/// ## fang +/// +/// For each request, get JWT token and verify based on given config and `Payload: Deserialize`. +/// +/// ## helper +/// +/// `.issue(/* Payload: Serialize */)` generates a JWT token on the config. +/// +///
+/// +/// ## default config +/// +/// - get token: from `Authorization: Bearer <here>` +/// - customizable by `.get_token_by( 〜 )` +/// - verifying algorithm: `HMAC-SHA256` +/// - `HMAC-SHA{256, 384, 512}` are available now +/// +///
+/// /// *example.rs* /// ```no_run /// use ohkami::prelude::*; @@ -34,7 +53,7 @@ use crate::{Fang, FangProc, IntoResponse, Request, Response}; /// Ohkami::new(( /// "/auth".GET(auth), /// "/private".By(Ohkami::with(/* -/// Automatically verify `Authorization` header +/// Automatically verify JWT token /// of a request and early returns an error /// response if it's invalid. /// If `Authorization` is valid, momorize the JWT diff --git a/ohkami/src/response/headers.rs b/ohkami/src/response/headers.rs index 4be8d838..c1e9bc0b 100644 --- a/ohkami/src/response/headers.rs +++ b/ohkami/src/response/headers.rs @@ -246,7 +246,7 @@ macro_rules! Header { } } }; -} Header! {44; +} Header! {45; AcceptRanges: b"Accept-Ranges", AccessControlAllowCredentials: b"Access-Control-Allow-Credentials", AccessControlAllowHeaders: b"Access-Control-Allow-Headers", @@ -289,6 +289,7 @@ macro_rules! Header { Upgrade: b"Upgrade", Vary: b"Vary", Via: b"Via", + WWWAuthenticate: b"WWW-Authenticate", XContentTypeOptions: b"X-Content-Type-Options", XFrameOptions: b"X-Frame-Options", } @@ -423,7 +424,7 @@ impl Headers { #[inline] pub(crate) fn new() -> Self { Self { - standard: Box::new([const {None}; N_SERVER_HEADERS]), + standard: Box::new([const {None}; N_SERVER_HEADERS]), insertlog: Vec::with_capacity(8), custom: None, setcookie: None, From 1969417ccf2da028871341ce616fecda2f4c1fd4 Mon Sep 17 00:00:00 2001 From: kana-rus Date: Wed, 26 Jun 2024 23:35:16 +0900 Subject: [PATCH 2/2] add Note section for doc & add basic_auth example --- examples/Cargo.toml | 3 ++- examples/basic_auth/Cargo.toml | 8 ++++++++ examples/basic_auth/src/main.rs | 16 ++++++++++++++++ ohkami/src/builtin/fang/basicauth.rs | 7 +++++++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 examples/basic_auth/Cargo.toml create mode 100644 examples/basic_auth/src/main.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index f4c68df6..7fbe4935 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -6,6 +6,7 @@ members = [ "hello", "openai", "realworld", + "basic_auth", "quick_start", "static_files", "json_response", @@ -20,4 +21,4 @@ tokio = { version = "1", features = ["full"] } sqlx = { version = "0.7.3", features = ["runtime-tokio-native-tls", "postgres", "macros", "chrono", "uuid"] } tracing = "0.1" tracing-subscriber = "0.3" -chrono = "0.4" +chrono = "0.4" \ No newline at end of file diff --git a/examples/basic_auth/Cargo.toml b/examples/basic_auth/Cargo.toml new file mode 100644 index 00000000..a76b297b --- /dev/null +++ b/examples/basic_auth/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "basic_auth" +version = "0.1.0" +edition = "2021" + +[dependencies] +ohkami = { workspace = true } +tokio = { workspace = true } \ No newline at end of file diff --git a/examples/basic_auth/src/main.rs b/examples/basic_auth/src/main.rs new file mode 100644 index 00000000..73600ea6 --- /dev/null +++ b/examples/basic_auth/src/main.rs @@ -0,0 +1,16 @@ +use ohkami::prelude::*; +use ohkami::builtin::fang::BasicAuth; + +#[tokio::main] +async fn main() { + Ohkami::new(( + "/hello".GET(|| async {"Hello, public!"}), + "/private".By(Ohkami::with( + BasicAuth { + username: "master of hello", + password: "world" + }, + "/hello".GET(|| async {"Hello, private :)"}) + )) + )).howl("localhost:8888").await +} diff --git a/ohkami/src/builtin/fang/basicauth.rs b/ohkami/src/builtin/fang/basicauth.rs index 243b0bb0..7bf730e6 100644 --- a/ohkami/src/builtin/fang/basicauth.rs +++ b/ohkami/src/builtin/fang/basicauth.rs @@ -8,6 +8,13 @@ use crate::prelude::*; /// - `[BasicAuth; N]` verifies each request to have one of the pairs of /// `username` and `password` /// +///
+/// +/// Note : **NEVER** hardcode `username` and `password` in your code +/// if you are pushing your source code to GitHub or other public repository!!! +/// +///
+/// /// *example* /// ```rust,no_run /// use ohkami::prelude::*;