diff --git a/Cargo.lock b/Cargo.lock index 995023f0..cd257a37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,8 @@ dependencies = [ "anyhow", "async-trait", "derive_more", + "rand", + "redis", "serde", "serde_json", "tracing", @@ -401,6 +403,7 @@ dependencies = [ "actix-identity", "actix-rt", "actix-service", + "actix-session", "actix-web", "actix-web-actors", "actix-web-flash-messages", @@ -434,6 +437,12 @@ dependencies = [ "validator", ] +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "argon2" version = "0.4.1" @@ -655,6 +664,20 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "config" version = "0.13.3" @@ -709,6 +732,16 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -909,6 +942,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -925,6 +979,15 @@ dependencies = [ "regex", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "flate2" version = "1.0.25" @@ -941,6 +1004,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -950,6 +1028,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.26" @@ -966,6 +1059,17 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +[[package]] +name = "futures-executor" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-intrusive" version = "0.4.2" @@ -977,6 +1081,12 @@ dependencies = [ "parking_lot 0.11.2", ] +[[package]] +name = "futures-io" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" + [[package]] name = "futures-macro" version = "0.3.26" @@ -1006,10 +1116,13 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1279,6 +1392,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "ipnet" version = "2.7.1" @@ -1365,6 +1488,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "local-channel" version = "0.1.3" @@ -1462,7 +1591,25 @@ dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] @@ -1537,6 +1684,51 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1588,7 +1780,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -1749,6 +1941,29 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redis" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152f3863635cbb76b73bc247845781098302c6c9ad2060e1a9a7de56840346b6" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures", + "futures-util", + "itoa", + "native-tls", + "percent-encoding", + "pin-project-lite", + "ryu", + "tokio", + "tokio-native-tls", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1860,6 +2075,20 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.36.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + [[package]] name = "rustls" version = "0.20.8" @@ -1887,6 +2116,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1919,6 +2157,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.16" @@ -2198,6 +2459,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -2307,7 +2581,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -2321,6 +2595,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.23.4" @@ -2578,6 +2862,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -2742,6 +3032,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index dd3a42a2..8e90c660 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ actix-service = "2.0.2" actix-identity = "0.5.2" actix-cors = "0.6.4" actix-web-flash-messages = { version = "0.4", features = ["cookies"] } +actix-session = { version = "0.7", features = ["redis-rs-tls-session"] } # serde serde_json = "1.0" diff --git a/src/api/mod.rs b/src/api/mod.rs index f0ff82f5..6ad6bb75 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,7 +1,5 @@ -mod password; mod token; mod user; -pub use password::password_scope; pub use token::token_scope; pub use user::user_scope; diff --git a/src/api/password.rs b/src/api/password.rs deleted file mode 100644 index b9e734cb..00000000 --- a/src/api/password.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::component::auth::AuthError; -use crate::state::State; - -use actix_web::web::Data; -use actix_web::{web, HttpRequest, HttpResponse, Scope}; - -pub fn password_scope() -> Scope { - web::scope("/api").service(web::resource("/password").route(web::post().to(change_password))) -} - -async fn change_password( - _req: HttpRequest, - _state: Data, -) -> Result { - todo!() -} diff --git a/src/api/user.rs b/src/api/user.rs index a19aaf9f..2b94af2e 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -1,9 +1,12 @@ use crate::component::auth::{ - login, logout, register, InputParamsError, LoggedUser, LoginRequest, RegisterRequestParams, + login, logout, register, ChangePasswordRequest, InputParamsError, LoggedUser, LoginRequest, + RegisterRequest, }; +use crate::component::token_state::SessionToken; use crate::domain::{UserEmail, UserName, UserPassword}; use crate::state::State; use actix_identity::Identity; +use actix_session::SessionInsertError; use actix_web::web::{Data, Json, Payload}; use actix_web::Result; use actix_web::{web, HttpResponse, Scope}; @@ -13,9 +16,14 @@ pub fn user_scope() -> Scope { .service(web::resource("/login").route(web::post().to(login_handler))) .service(web::resource("/logout").route(web::get().to(logout_handler))) .service(web::resource("/register").route(web::post().to(register_handler))) + .service(web::resource("/password").route(web::post().to(change_password))) } -async fn login_handler(req: Json, state: Data) -> Result { +async fn login_handler( + req: Json, + state: Data, + session: SessionToken, +) -> Result { let params = req.into_inner(); let email = UserEmail::parse(params.email) .map_err(|e| InputParamsError::InvalidEmail(e))? @@ -23,7 +31,15 @@ async fn login_handler(req: Json, state: Data) -> Result) -> Result, - state: Data, -) -> Result { +async fn register_handler(req: Json, state: Data) -> Result { let params = req.into_inner(); let name = UserName::parse(params.name) .map_err(|e| InputParamsError::InvalidName(e))? @@ -59,3 +72,10 @@ async fn register_handler( Ok(HttpResponse::Ok().json(resp)) } + +async fn change_password( + req: Json, + state: Data, +) -> Result { + todo!() +} diff --git a/src/application.rs b/src/application.rs index a15fd18f..67caf4c7 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,3 +1,4 @@ +use crate::api::{token_scope, user_scope}; use actix_identity::IdentityMiddleware; use actix_web::{dev::Server, middleware, web, web::Data, App, HttpServer}; use sqlx::{postgres::PgPoolOptions, PgPool}; @@ -5,8 +6,6 @@ use std::net::TcpListener; use std::sync::Arc; use tracing_actix_web::TracingLogger; -use crate::api::{password_scope, token_scope, user_scope}; - use crate::config::config::{Config, DatabaseSetting}; use crate::middleware::cors::default_cors; diff --git a/src/component/auth/password.rs b/src/component/auth/password.rs index 83930467..835ac4df 100644 --- a/src/component/auth/password.rs +++ b/src/component/auth/password.rs @@ -4,6 +4,7 @@ use anyhow::Context; use argon2::password_hash::SaltString; use argon2::{Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version}; use secrecy::{ExposeSecret, Secret}; +use serde::Deserialize; use sqlx::PgPool; pub struct Credentials { @@ -11,7 +12,7 @@ pub struct Credentials { pub password: Secret, } -#[tracing::instrument(name = "Validate credentials", skip(credentials, pool))] +#[tracing::instrument(skip(credentials, pool))] pub async fn validate_credentials( credentials: Credentials, pool: &PgPool, @@ -41,7 +42,7 @@ pub async fn validate_credentials( .map_err(AuthError::InvalidCredentials) } -#[tracing::instrument(name = "Change password", skip(password, pool))] +#[tracing::instrument(skip(password, pool))] pub async fn change_password( uid: uuid::Uuid, password: Secret, @@ -80,7 +81,7 @@ pub fn compute_hash_password(password: &[u8]) -> Result { .to_string()) } -#[tracing::instrument(name = "Get stored credentials", skip(email, pool))] +#[tracing::instrument(skip(email, pool))] async fn get_stored_credentials( email: &str, pool: &PgPool, @@ -100,10 +101,6 @@ async fn get_stored_credentials( Ok(row) } -#[tracing::instrument( - name = "Validate credentials", - skip(expected_password_hash, password_candidate) -)] fn verify_password_hash( expected_password_hash: Secret, password_candidate: Secret, @@ -118,3 +115,10 @@ fn verify_password_hash( ) .context("Invalid password.") } + +#[derive(Default, Deserialize, Debug)] +pub struct ChangePasswordRequest { + pub new_password: String, + pub current_password: String, + pub current_password_check: String, +} diff --git a/src/component/auth/user.rs b/src/component/auth/user.rs index 2b4cec18..64f25f7b 100644 --- a/src/component/auth/user.rs +++ b/src/component/auth/user.rs @@ -10,7 +10,8 @@ use chrono::Utc; use chrono::{Duration, Local}; use futures_util::future::{ready, Ready}; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; -use secrecy::Secret; +use secrecy::zeroize::DefaultIsZeroes; +use secrecy::{Secret, Zeroize}; use serde::{Deserialize, Serialize}; use sqlx::types::uuid; use sqlx::{PgPool, Postgres, Transaction}; @@ -23,7 +24,7 @@ pub async fn login( cache: Arc>, email: String, password: String, -) -> Result { +) -> Result<(LoginResponse, Secret), AuthError> { let credentials = Credentials { email, password: Secret::new(password), @@ -32,14 +33,16 @@ pub async fn login( match validate_credentials(credentials, &pg_pool).await { Ok(uid) => { let uid = uid.to_string(); - let token = Token::create_token(&uid)?.into(); + let token = Token::create_token(&uid)?; let logged_user = LoggedUser::new(uid.clone()); cache.write().await.authorized(logged_user); - - Ok(LoginResponse { - token, - uid, - }) + Ok(( + LoginResponse { + token: token.clone().into(), + uid, + }, + Secret::new(token), + )) } Err(err) => Err(err), } @@ -116,26 +119,26 @@ async fn is_email_exist( Ok(result.is_some()) } -#[derive(Default, Serialize, Deserialize, Debug)] +#[derive(Default, Deserialize, Debug)] pub struct LoginRequest { pub email: String, pub password: String, } -#[derive(Default, Serialize, Deserialize, Debug)] +#[derive(Default, Serialize, Debug)] pub struct LoginResponse { pub token: String, pub uid: String, } -#[derive(Default, Serialize, Deserialize, Debug)] -pub struct RegisterRequestParams { +#[derive(Default, Deserialize, Debug)] +pub struct RegisterRequest { pub email: String, pub password: String, pub name: String, } -#[derive(Default, Serialize, Deserialize, Debug)] +#[derive(Default, Serialize, Debug)] pub struct RegisterResponse { pub token: String, } @@ -227,8 +230,15 @@ impl Claim { } } -#[derive(Clone)] +#[derive(Clone, Default, Serialize, Deserialize)] pub struct Token(pub String); + +impl Zeroize for Token { + fn zeroize(&mut self) { + self.0.zeroize() + } +} + impl Token { pub fn create_token(user_id: &str) -> Result { let claims = Claim::with_user_id(user_id); diff --git a/src/component/mod.rs b/src/component/mod.rs index 0e4a05d5..79bb8910 100644 --- a/src/component/mod.rs +++ b/src/component/mod.rs @@ -1 +1,2 @@ pub mod auth; +pub mod token_state; diff --git a/src/component/token_state.rs b/src/component/token_state.rs new file mode 100644 index 00000000..461b2adf --- /dev/null +++ b/src/component/token_state.rs @@ -0,0 +1,39 @@ +use crate::component::auth::Token; +use actix_session::SessionExt; +use actix_session::{Session, SessionGetError, SessionInsertError}; +use actix_web::dev::Payload; +use actix_web::{FromRequest, HttpRequest}; +use secrecy::{ExposeSecret, Secret}; +use std::future::{ready, Ready}; +use uuid::Uuid; + +pub struct SessionToken(Session); + +impl SessionToken { + const TOKEN_ID_KEY: &'static str = "token"; + + pub fn renew(&self) { + self.0.renew(); + } + + pub fn insert_token(&self, token: Secret) -> Result<(), SessionInsertError> { + self.0.insert(Self::TOKEN_ID_KEY, token.expose_secret()) + } + + pub fn get_token(&self) -> Result, SessionGetError> { + self.0.get(Self::TOKEN_ID_KEY) + } + + pub fn log_out(self) { + self.0.purge() + } +} + +impl FromRequest for SessionToken { + type Error = ::Error; + type Future = Ready>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + ready(Ok(SessionToken(req.get_session()))) + } +}