diff --git a/services/backup/Cargo.lock b/services/backup/Cargo.lock --- a/services/backup/Cargo.lock +++ b/services/backup/Cargo.lock @@ -45,7 +45,7 @@ "actix-service", "actix-utils", "ahash 0.8.3", - "base64", + "base64 0.21.2", "bitflags 1.3.2", "brotli", "bytes", @@ -236,6 +236,21 @@ "syn 1.0.109", ] +[[package]] +name = "actix-web-httpauth" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dda62cf04bc3a9ad2ea8f314f721951cfdb4cdacec4e984d20e77c7bb170991" +dependencies = [ + "actix-utils", + "actix-web", + "base64 0.13.1", + "futures-core", + "futures-util", + "log", + "pin-project-lite", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -754,6 +769,12 @@ "uuid", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.2" @@ -928,10 +949,12 @@ "actix-cors", "actix-multipart", "actix-web", + "actix-web-httpauth", "anyhow", "aws-config", "aws-sdk-dynamodb", "aws-types", + "base64 0.21.2", "chrono", "derive_more", "futures-core", @@ -2051,7 +2074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64", + "base64 0.21.2", "bytes", "encoding_rs", "futures-core", @@ -2158,7 +2181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64", + "base64 0.21.2", ] [[package]] diff --git a/services/blob/Cargo.lock b/services/blob/Cargo.lock --- a/services/blob/Cargo.lock +++ b/services/blob/Cargo.lock @@ -236,6 +236,21 @@ "syn 1.0.103", ] +[[package]] +name = "actix-web-httpauth" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dda62cf04bc3a9ad2ea8f314f721951cfdb4cdacec4e984d20e77c7bb170991" +dependencies = [ + "actix-utils", + "actix-web", + "base64 0.13.1", + "futures-core", + "futures-util", + "log", + "pin-project-lite", +] + [[package]] name = "adler" version = "1.0.2" @@ -978,15 +993,19 @@ "actix-cors", "actix-multipart", "actix-web", + "actix-web-httpauth", "anyhow", "aws-config", "aws-sdk-dynamodb", "aws-types", + "base64 0.21.0", "chrono", "derive_more", "futures-core", "futures-util", "http", + "serde", + "serde_json", "tokio-stream", "tracing", ] diff --git a/services/comm-services-lib/Cargo.lock b/services/comm-services-lib/Cargo.lock --- a/services/comm-services-lib/Cargo.lock +++ b/services/comm-services-lib/Cargo.lock @@ -45,7 +45,7 @@ "actix-service", "actix-utils", "ahash 0.8.3", - "base64", + "base64 0.21.0", "bitflags", "brotli", "bytes", @@ -236,6 +236,21 @@ "syn 1.0.109", ] +[[package]] +name = "actix-web-httpauth" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dda62cf04bc3a9ad2ea8f314f721951cfdb4cdacec4e984d20e77c7bb170991" +dependencies = [ + "actix-utils", + "actix-web", + "base64 0.13.1", + "futures-core", + "futures-util", + "log", + "pin-project-lite", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -652,6 +667,12 @@ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.0" @@ -782,10 +803,12 @@ "actix-cors", "actix-multipart", "actix-web", + "actix-web-httpauth", "anyhow", "aws-config", "aws-sdk-dynamodb", "aws-types", + "base64 0.21.0", "chrono", "derive_more", "futures-core", @@ -1758,7 +1781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64", + "base64 0.21.0", "bytes", "encoding_rs", "futures-core", @@ -1866,7 +1889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64", + "base64 0.21.0", ] [[package]] diff --git a/services/comm-services-lib/Cargo.toml b/services/comm-services-lib/Cargo.toml --- a/services/comm-services-lib/Cargo.toml +++ b/services/comm-services-lib/Cargo.toml @@ -9,8 +9,6 @@ "dep:reqwest", "dep:futures-core", "dep:futures-util", - "dep:serde", - "dep:serde_json", "dep:tokio", ] http = [ @@ -21,12 +19,16 @@ "dep:futures-util", "dep:http", "dep:tokio-stream", + "dep:actix-web-httpauth", ] [dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" aws-config = "0.55" aws-sdk-dynamodb = "0.27" aws-types = "0.55" +base64 = "0.21" chrono = "0.4" derive_more = "0.99" tracing = "0.1" @@ -39,12 +41,11 @@ "multipart", "stream", ], optional = true } -serde = { version = "1.0", features = ["derive"], optional = true } -serde_json = { version = "1.0", optional = true } tokio = { version = "1.32", optional = true } # http dependencies actix-cors = { version = "0.6", optional = true } actix-web = { version = "4.3", optional = true } http = { version = "0.2.9", optional = true } +actix-web-httpauth = { version = "0.8.0", optional = true } actix-multipart = { version = "0.6", optional = true } tokio-stream = { version = "0.1.14", optional = true } diff --git a/services/comm-services-lib/src/auth.rs b/services/comm-services-lib/src/auth.rs new file mode 100644 --- /dev/null +++ b/services/comm-services-lib/src/auth.rs @@ -0,0 +1,79 @@ +use base64::{prelude::BASE64_STANDARD, Engine}; +use derive_more::{Display, Error, From}; +use serde::{Deserialize, Serialize}; +use std::{str::FromStr, string::FromUtf8Error}; + +/// This implements [`actix_web::FromRequest`], so it can be used to extract user +/// identity information from HTTP requests. +/// # Example +/// ```ignore +/// pub async fn request_handler( +/// user: UserIdentity, +/// ) -> Result { +/// Ok(HttpResponse::Ok().finish()) +/// } +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct UserIdentity { + #[serde(rename = "userID")] + pub user_id: String, + #[serde(rename = "accessToken")] + pub access_token: String, + #[serde(rename = "deviceID")] + pub device_id: String, +} + +impl UserIdentity { + /// Gets the access token value, usable in bearer authorization + /// + /// # Example + /// ```ignore + /// reqwest::get("url").beaerer_auth(user.as_authorization_token()?).send().await?; + /// ``` + pub fn as_authorization_token(&self) -> Result { + let json = serde_json::to_string(self)?; + let base64_str = BASE64_STANDARD.encode(json); + Ok(base64_str) + } +} + +#[derive(Debug, Display, Error, From)] +pub enum UserIdentityParseError { + Base64DecodeError(base64::DecodeError), + Utf8DecodeError(FromUtf8Error), + JsonParseError(serde_json::Error), +} + +/// Parsing of [UserIdentity] from bearer token +impl FromStr for UserIdentity { + type Err = UserIdentityParseError; + + fn from_str(s: &str) -> Result { + let bytes = BASE64_STANDARD.decode(s)?; + let text = String::from_utf8(bytes)?; + let user = serde_json::from_str(&text)?; + Ok(user) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_user_identity_parsing() { + let identity = UserIdentity { + user_id: "user".to_string(), + access_token: "token".to_string(), + device_id: "device".to_string(), + }; + let json = + r#"{"userID": "user", "accessToken": "token", "deviceID": "device"}"#; + let encoded = BASE64_STANDARD.encode(json); + + let parsed_identity = encoded.parse::(); + assert!(parsed_identity.is_ok(), "Parse error: {parsed_identity:?}"); + + assert_eq!(parsed_identity.unwrap(), identity); + } +} diff --git a/services/comm-services-lib/src/http.rs b/services/comm-services-lib/src/http.rs --- a/services/comm-services-lib/src/http.rs +++ b/services/comm-services-lib/src/http.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod multipart; use crate::tools::BoxedError; diff --git a/services/comm-services-lib/src/http/auth.rs b/services/comm-services-lib/src/http/auth.rs new file mode 100644 --- /dev/null +++ b/services/comm-services-lib/src/http/auth.rs @@ -0,0 +1,108 @@ +use actix_web::{ + body::{EitherBody, MessageBody}, + dev::{Service, ServiceRequest, ServiceResponse, Transform}, + FromRequest, HttpMessage, +}; +use actix_web_httpauth::{ + extractors::{bearer::BearerAuth, AuthenticationError}, + headers::www_authenticate::bearer::Bearer, + middleware::HttpAuthentication, +}; +use std::{ + future::{ready, Ready}, + str::FromStr, +}; +use tracing::debug; + +use crate::auth::UserIdentity; + +impl FromRequest for UserIdentity { + type Error = actix_web::Error; + type Future = Ready>; + + fn from_request( + req: &actix_web::HttpRequest, + _: &mut actix_web::dev::Payload, + ) -> Self::Future { + if let Some(user) = req.extensions().get::() { + return ready(Ok(user.clone())); + } + + let f = || { + let bearer = BearerAuth::extract(req).into_inner()?; + let user = match UserIdentity::from_str(bearer.token()) { + Ok(user) => user, + Err(err) => { + debug!("HTTP authorization error: {err}"); + return Err(AuthenticationError::new(Bearer::default()).into()); + } + }; + + Ok(user) + }; + + ready(f()) + } +} + +pub async fn validation_function( + req: ServiceRequest, + bearer: BearerAuth, +) -> Result { + let user = match UserIdentity::from_str(bearer.token()) { + Ok(user) => user, + Err(err) => { + debug!("HTTP authorization error: {err}"); + return Err((AuthenticationError::new(Bearer::default()).into(), req)); + } + }; + + // TODO: call identity service, for now just allow every request + req.extensions_mut().insert(user); + + Ok(req) +} + +/// Use this to add Authentication Middleware. It's going to parse Authorization +/// header and call the identity service to check if the provided credentials +/// are correct. If not it's going to reject the request. +/// +/// # Example +/// ```ignore +/// let auth_middleware = get_comm_authentication_middleware(); +/// App::new().wrap(auth_middleware); +/// ``` +/// If you don't want all of the routes to require authentication you can wrap +/// individual resources or scopes: +/// ```ignore +/// App::new().service( +/// web::resource("/endpoint").route(web::get().to(handler)).wrap(auth_middleware), +/// ) +/// ``` +// This type is very complicated, but unfortunately typing this directly +// requires https://github.com/rust-lang/rust/issues/99697 to be merged. +// The issue is that we can't specify the second generic argument of +// HttpAuthentication, because it look something like this: +// ``` +// impl Fn(ServiceRequest, BearerAuth) -> impl Future< +// Output = Result, +// > +// `` +// which isn't valid (until the linked issue is merged). +pub fn get_comm_authentication_middleware() -> impl Transform< + S, + ServiceRequest, + Response = ServiceResponse>, + Error = actix_web::Error, + InitError = (), +> + 'static +where + B: MessageBody + 'static, + S: Service< + ServiceRequest, + Response = ServiceResponse, + Error = actix_web::Error, + > + 'static, +{ + HttpAuthentication::bearer(validation_function) +} diff --git a/services/comm-services-lib/src/lib.rs b/services/comm-services-lib/src/lib.rs --- a/services/comm-services-lib/src/lib.rs +++ b/services/comm-services-lib/src/lib.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod blob; pub mod constants; pub mod database; diff --git a/services/feature-flags/Cargo.lock b/services/feature-flags/Cargo.lock --- a/services/feature-flags/Cargo.lock +++ b/services/feature-flags/Cargo.lock @@ -731,8 +731,11 @@ "aws-config", "aws-sdk-dynamodb", "aws-types", + "base64", "chrono", "derive_more", + "serde", + "serde_json", "tracing", ]