feat: change password

This commit is contained in:
nathan 2023-03-13 09:17:14 +08:00
parent 0a08a34a00
commit 25b1f8eeb7
10 changed files with 412 additions and 51 deletions

311
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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;

View File

@ -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<State>,
) -> Result<HttpResponse, AuthError> {
todo!()
}

View File

@ -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<LoginRequest>, state: Data<State>) -> Result<HttpResponse> {
async fn login_handler(
req: Json<LoginRequest>,
state: Data<State>,
session: SessionToken,
) -> Result<HttpResponse> {
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<LoginRequest>, state: Data<State>) -> Result<Ht
let password = UserPassword::parse(params.password)
.map_err(|_| InputParamsError::InvalidPassword)?
.0;
let resp = login(state.pg_pool.clone(), state.cache.clone(), email, password).await?;
let (resp, token) = login(state.pg_pool.clone(), state.cache.clone(), email, password).await?;
// Renews the session key, assigning existing session state to new key.
session.renew();
if let Err(err) = session.insert_token(token) {
// It needs to navigate to login page in web application
tracing::error!("Insert session failed: {}", err);
}
Ok(HttpResponse::Ok().json(resp))
}
@ -33,10 +49,7 @@ async fn logout_handler(logged_user: LoggedUser, state: Data<State>) -> Result<H
}
#[tracing::instrument(level = "debug", skip(state))]
async fn register_handler(
req: Json<RegisterRequestParams>,
state: Data<State>,
) -> Result<HttpResponse> {
async fn register_handler(req: Json<RegisterRequest>, state: Data<State>) -> Result<HttpResponse> {
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<ChangePasswordRequest>,
state: Data<State>,
) -> Result<HttpResponse> {
todo!()
}

View File

@ -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;

View File

@ -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<String>,
}
#[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<String>,
@ -80,7 +81,7 @@ pub fn compute_hash_password(password: &[u8]) -> Result<String, anyhow::Error> {
.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<String>,
password_candidate: Secret<String>,
@ -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,
}

View File

@ -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<RwLock<Cache>>,
email: String,
password: String,
) -> Result<LoginResponse, AuthError> {
) -> Result<(LoginResponse, Secret<Token>), 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<Self, AuthError> {
let claims = Claim::with_user_id(user_id);

View File

@ -1 +1,2 @@
pub mod auth;
pub mod token_state;

View File

@ -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<Token>) -> Result<(), SessionInsertError> {
self.0.insert(Self::TOKEN_ID_KEY, token.expose_secret())
}
pub fn get_token(&self) -> Result<Option<String>, SessionGetError> {
self.0.get(Self::TOKEN_ID_KEY)
}
pub fn log_out(self) {
self.0.purge()
}
}
impl FromRequest for SessionToken {
type Error = <Session as FromRequest>::Error;
type Future = Ready<Result<SessionToken, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
ready(Ok(SessionToken(req.get_session())))
}
}