filigree/auth/
password.rs1use std::{ops::Deref, sync::Arc};
2
3use argon2::{
4 password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
5 Argon2,
6};
7use error_stack::{Report, ResultExt};
8use serde_json::json;
9use sqlx::PgPool;
10use tower_cookies::Cookies;
11use tracing::instrument;
12use uuid::Uuid;
13
14use super::{sessions::SessionBackend, AuthError, EmailAndPassword, UserId};
15use crate::errors::FormDataResponse;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct HashedPassword(pub String);
21
22impl Deref for HashedPassword {
23 type Target = String;
24
25 fn deref(&self) -> &Self::Target {
26 &self.0
27 }
28}
29
30pub async fn new_hash(password: String) -> Result<HashedPassword, AuthError> {
32 let salt = uuid::Uuid::new_v4();
33 hash_password(password, salt).await
34}
35
36#[instrument]
37async fn hash_password(password: String, salt: Uuid) -> Result<HashedPassword, AuthError> {
38 let hash = tokio::task::spawn_blocking(move || {
39 let saltstring = SaltString::encode_b64(salt.as_bytes())
40 .map_err(|e| AuthError::PasswordHasherError(e.to_string()))?;
41
42 let hash = Argon2::default()
43 .hash_password(password.as_bytes(), saltstring.as_salt())
44 .map_err(|e| AuthError::PasswordHasherError(e.to_string()))?;
45
46 Ok::<_, AuthError>(hash.to_string())
47 })
48 .await
49 .map_err(|e| AuthError::PasswordHasherError(e.to_string()))??;
50
51 Ok(HashedPassword(hash))
52}
53
54pub async fn verify_password(password: String, hash_str: HashedPassword) -> Result<(), AuthError> {
56 tokio::task::spawn_blocking(move || {
57 let hash = PasswordHash::new(&hash_str)
58 .map_err(|e| AuthError::PasswordHasherError(e.to_string()))?;
59
60 Argon2::default()
61 .verify_password(password.as_bytes(), &hash)
62 .map_err(|_| AuthError::IncorrectPassword)
63 })
64 .await
65 .map_err(|e| AuthError::PasswordHasherError(e.to_string()))??;
66
67 Ok(())
68}
69
70pub async fn lookup_user_from_email_and_password(
72 db: &PgPool,
73 email_and_password: EmailAndPassword,
74) -> Result<UserId, Report<AuthError>> {
75 if email_and_password.password.is_empty() {
76 Err(AuthError::Unauthenticated)?;
79 }
80
81 let user_info = sqlx::query!(
82 r#"SELECT user_id as "user_id: UserId", password_hash, email_logins.verified
83 FROM email_logins
84 JOIN users ON users.id = email_logins.user_id
85 WHERE email_logins.email = $1"#,
86 email_and_password.email
87 )
88 .fetch_optional(db)
89 .await
90 .change_context(AuthError::Db)?
91 .ok_or(AuthError::UserNotFound)?;
92
93 let password_hash = HashedPassword(user_info.password_hash.unwrap_or_default());
94
95 verify_password(email_and_password.password, password_hash).await?;
96
97 if !user_info.verified {
98 return Err(Report::new(AuthError::NotVerified))?;
99 }
100
101 Ok(user_info.user_id)
102}
103
104pub async fn login_with_password(
108 session_backend: &SessionBackend,
109 cookies: &Cookies,
110 email_and_password: EmailAndPassword,
111) -> Result<(), Report<AuthError>> {
112 let user_id =
113 lookup_user_from_email_and_password(&session_backend.db, email_and_password.clone())
114 .await
115 .attach_lazy(|| {
116 FormDataResponse::new(Arc::new(json!({ "email": email_and_password.email })))
117 })?;
118
119 session_backend
120 .create_session(&cookies, &user_id)
121 .await
122 .change_context(AuthError::SessionBackend)?;
123 Ok(())
124}
125
126pub async fn create_reset_token(db: &sqlx::PgPool, email: &str) -> Result<Uuid, Report<AuthError>> {
128 let token = Uuid::new_v4();
129
130 let result = sqlx::query!(
131 "UPDATE email_logins
132 SET reset_token = $2,
133 reset_expires_at = now() + '1 hour'::interval
134 WHERE email = $1",
135 email,
136 &token
137 )
138 .execute(db)
139 .await
140 .change_context(AuthError::Db)?;
141
142 if result.rows_affected() == 0 {
143 return Err(Report::new(AuthError::Unauthenticated));
144 }
145
146 Ok(token)
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[tokio::test]
154 #[cfg_attr(
155 not(any(feature = "test_slow", feature = "test_password")),
156 ignore = "slow password test"
157 )]
158 async fn good_password() -> Result<(), AuthError> {
159 let hash = new_hash("abcdef".into()).await?;
160 verify_password("abcdef".to_string(), hash).await
161 }
162
163 #[tokio::test]
164 #[cfg_attr(
165 not(any(feature = "test_slow", feature = "test_password")),
166 ignore = "slow password test"
167 )]
168 async fn bad_password() -> Result<(), AuthError> {
169 let hash = new_hash("abcdef".into()).await?;
170 verify_password("abcdefg".to_string(), hash)
171 .await
172 .expect_err("non-matching password");
173 Ok(())
174 }
175
176 #[tokio::test]
178 #[cfg_attr(
179 not(any(feature = "test_slow", feature = "test_password")),
180 ignore = "slow password test"
181 )]
182 async fn unique_password_salt() {
183 let p1 = new_hash("abc".into()).await.unwrap();
184 let p2 = new_hash("abc".into()).await.unwrap();
185 assert_ne!(p1, p2);
186 }
187}