diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml new file mode 100644 index 0000000..3b46bc4 --- /dev/null +++ b/crates/auth/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rustical_auth" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "4.4.0" +actix-web-httpauth = "0.8.0" +derive_more = "0.99.17" +futures-util = "0.3.28" +password-auth = "1.0.0" +serde = { version = "1.0.188", features = ["derive"] } diff --git a/crates/auth/src/error.rs b/crates/auth/src/error.rs new file mode 100644 index 0000000..07526cc --- /dev/null +++ b/crates/auth/src/error.rs @@ -0,0 +1,33 @@ +use actix_web::{http::StatusCode, HttpResponse}; +use derive_more::{Display, Error}; + +#[derive(Debug, Display, Error)] +pub enum Error { + #[display(fmt = "Internal server error")] + InternalError, + #[display(fmt = "Not found")] + NotFound, + #[display(fmt = "Bad request")] + BadRequest, + Unauthorized, +} + +impl actix_web::error::ResponseError for Error { + fn status_code(&self) -> StatusCode { + match *self { + Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotFound => StatusCode::NOT_FOUND, + Self::BadRequest => StatusCode::BAD_REQUEST, + Self::Unauthorized => StatusCode::UNAUTHORIZED, + } + } + + fn error_response(&self) -> HttpResponse { + match self { + Error::Unauthorized => HttpResponse::build(self.status_code()) + .append_header(("WWW-Authenticate", "Basic")) + .body(self.to_string()), + _ => HttpResponse::build(self.status_code()).body(self.to_string()), + } + } +} diff --git a/crates/auth/src/extractor.rs b/crates/auth/src/extractor.rs new file mode 100644 index 0000000..5ad0759 --- /dev/null +++ b/crates/auth/src/extractor.rs @@ -0,0 +1,64 @@ +use actix_web::{dev::Payload, web::Data, FromRequest, HttpRequest}; +use futures_util::{Future, FutureExt}; +use std::marker::PhantomData; +use std::pin::Pin; + +use super::{CheckAuthentication, AuthInfo}; + +pub struct AuthInfoExtractor { + pub inner: AuthInfo, + pub _provider_type: PhantomData, +} + +impl From for AuthInfoExtractor { + fn from(value: AuthInfo) -> Self { + AuthInfoExtractor { + inner: value, + _provider_type: PhantomData::, + } + } +} + +pub struct AuthInfoExtractorFuture +where + A: CheckAuthentication, +{ + future: Pin>, +} + +impl Future for AuthInfoExtractorFuture +where + A: CheckAuthentication, +{ + type Output = Result, A::Error>; + + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + match self.get_mut().future.poll_unpin(cx) { + std::task::Poll::Pending => std::task::Poll::Pending, + std::task::Poll::Ready(result) => { + std::task::Poll::Ready(result.map(|auth_info| auth_info.into())) + } + } + } +} + +impl FromRequest for AuthInfoExtractor +where + A: CheckAuthentication, +{ + type Error = A::Error; + type Future = AuthInfoExtractorFuture; + + fn extract(req: &HttpRequest) -> Self::Future { + let a = req.app_data::>().unwrap().validate(req); + Self::Future { + future: Box::pin(a), + } + } + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + Self::extract(req) + } +} diff --git a/crates/auth/src/htpasswd.rs b/crates/auth/src/htpasswd.rs new file mode 100644 index 0000000..b3db7c3 --- /dev/null +++ b/crates/auth/src/htpasswd.rs @@ -0,0 +1,52 @@ +use actix_web::{http::header::Header, HttpRequest}; +use actix_web_httpauth::headers::authorization::{Authorization, Basic}; +use futures_util::future::{err, ok, Ready}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AuthInfo, CheckAuthentication}; + +#[derive(Debug)] +pub struct HtpasswdAuth { + pub config: HtpasswdAuthConfig, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct HtpasswdAuthUserConfig { + password: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct HtpasswdAuthConfig { + pub users: HashMap, +} + +impl CheckAuthentication for HtpasswdAuth { + type Error = crate::error::Error; + type Future = Ready>; + + fn validate(&self, req: &HttpRequest) -> Self::Future { + if let Ok(auth) = Authorization::::parse(req) { + let user_id = auth.as_ref().user_id(); + // Map None to empty password + let password = auth.as_ref().password().unwrap_or_default(); + + let user_config = if let Some(user_config) = self.config.users.get(user_id) { + user_config + } else { + return err(crate::error::Error::Unauthorized); + }; + + if let Err(e) = password_auth::verify_password(password, &user_config.password) { + dbg!(e); + return err(crate::error::Error::Unauthorized); + } + + ok(AuthInfo { + user_id: user_id.to_string(), + }) + } else { + err(crate::error::Error::Unauthorized) + } + } +} diff --git a/crates/auth/src/lib.rs b/crates/auth/src/lib.rs new file mode 100644 index 0000000..7f328b7 --- /dev/null +++ b/crates/auth/src/lib.rs @@ -0,0 +1,46 @@ +use actix_web::{HttpRequest, ResponseError}; +use futures_util::{future::Ready, Future}; + +pub use extractor::AuthInfoExtractor; +pub use htpasswd::{HtpasswdAuth, HtpasswdAuthConfig}; +pub use none::NoneAuth; +pub mod error; +pub mod extractor; +pub mod htpasswd; +pub mod none; + +pub struct AuthInfo { + pub user_id: String, +} + +pub trait CheckAuthentication: Send + Sync + 'static { + type Error: ResponseError; + type Future: Future> + where + Self: Sized; + + fn validate(&self, req: &HttpRequest) -> Self::Future + where + Self: Sized; +} + +#[derive(Debug)] +pub enum AuthProvider { + Htpasswd(HtpasswdAuth), + None(NoneAuth), +} + +impl CheckAuthentication for AuthProvider { + type Error = crate::error::Error; + type Future = Ready>; + + fn validate(&self, req: &HttpRequest) -> Self::Future + where + Self: Sized, + { + match self { + Self::Htpasswd(auth) => auth.validate(req), + Self::None(auth) => auth.validate(req), + } + } +} diff --git a/crates/auth/src/none.rs b/crates/auth/src/none.rs new file mode 100644 index 0000000..11cabbb --- /dev/null +++ b/crates/auth/src/none.rs @@ -0,0 +1,23 @@ +use actix_web::{http::header::Header, HttpRequest}; +use actix_web_httpauth::headers::authorization::{Authorization, Basic}; +use futures_util::future::{err, ok, Ready}; + +use super::{AuthInfo, CheckAuthentication}; + +#[derive(Debug, Clone)] +pub struct NoneAuth; + +impl CheckAuthentication for NoneAuth { + type Error = crate::error::Error; + type Future = Ready>; + + fn validate(&self, req: &HttpRequest) -> Self::Future { + if let Ok(auth) = Authorization::::parse(req) { + ok(AuthInfo { + user_id: auth.as_ref().user_id().to_string(), + }) + } else { + err(crate::error::Error::Unauthorized) + } + } +}