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.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..7bf730e6
--- /dev/null
+++ b/ohkami/src/builtin/fang/basicauth.rs
@@ -0,0 +1,115 @@
+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`
+///
+///
+///
+/// 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::*;
+/// 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,