torrust_index_backend/services/
authentication.rs

1//! Authentication services.
2use std::sync::Arc;
3
4use argon2::{Argon2, PasswordHash, PasswordVerifier};
5use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
6use pbkdf2::Pbkdf2;
7
8use super::user::{DbUserProfileRepository, DbUserRepository};
9use crate::config::Configuration;
10use crate::databases::database::{Database, Error};
11use crate::errors::ServiceError;
12use crate::models::user::{UserAuthentication, UserClaims, UserCompact, UserId};
13use crate::utils::clock;
14
15pub struct Service {
16    configuration: Arc<Configuration>,
17    json_web_token: Arc<JsonWebToken>,
18    user_repository: Arc<DbUserRepository>,
19    user_profile_repository: Arc<DbUserProfileRepository>,
20    user_authentication_repository: Arc<DbUserAuthenticationRepository>,
21}
22
23impl Service {
24    pub fn new(
25        configuration: Arc<Configuration>,
26        json_web_token: Arc<JsonWebToken>,
27        user_repository: Arc<DbUserRepository>,
28        user_profile_repository: Arc<DbUserProfileRepository>,
29        user_authentication_repository: Arc<DbUserAuthenticationRepository>,
30    ) -> Self {
31        Self {
32            configuration,
33            json_web_token,
34            user_repository,
35            user_profile_repository,
36            user_authentication_repository,
37        }
38    }
39
40    /// Authenticate user with username and password.
41    /// It returns a JWT token and a compact user profile.
42    ///
43    /// # Errors
44    ///
45    /// It returns:
46    ///
47    /// * A `ServiceError::WrongPasswordOrUsername` if unable to get user profile.
48    /// * A `ServiceError::InternalServerError` if unable to get user authentication data from the user id.
49    /// * A `ServiceError::EmailNotVerified` if the email should be, but is not verified.
50    /// * An error if unable to verify the password.
51    /// * An error if unable to get the user data from the database.
52    pub async fn login(&self, username: &str, password: &str) -> Result<(String, UserCompact), ServiceError> {
53        // Get the user profile from database
54        let user_profile = self
55            .user_profile_repository
56            .get_user_profile_from_username(username)
57            .await
58            .map_err(|_| ServiceError::WrongPasswordOrUsername)?;
59
60        // Should not be able to fail if user_profile succeeded
61        let user_authentication = self
62            .user_authentication_repository
63            .get_user_authentication_from_id(&user_profile.user_id)
64            .await
65            .map_err(|_| ServiceError::InternalServerError)?;
66
67        verify_password(password.as_bytes(), &user_authentication)?;
68
69        let settings = self.configuration.settings.read().await;
70
71        // Fail login if email verification is required and this email is not verified
72        if settings.mail.email_verification_enabled && !user_profile.email_verified {
73            return Err(ServiceError::EmailNotVerified);
74        }
75
76        // Drop read lock on settings
77        drop(settings);
78
79        let user_compact = self.user_repository.get_compact(&user_profile.user_id).await?;
80
81        // Sign JWT with compact user details as payload
82        let token = self.json_web_token.sign(user_compact.clone()).await;
83
84        Ok((token, user_compact))
85    }
86
87    /// Renew a supplied JWT.
88    ///
89    /// # Errors
90    ///
91    /// This function will return an error if:
92    ///
93    /// * Unable to verify the supplied payload as a valid jwt.
94    /// * Unable to get user data from the database.
95    pub async fn renew_token(&self, token: &str) -> Result<(String, UserCompact), ServiceError> {
96        const ONE_WEEK_IN_SECONDS: u64 = 604_800;
97
98        // Verify if token is valid
99        let claims = self.json_web_token.verify(token).await?;
100
101        let user_compact = self.user_repository.get_compact(&claims.user.user_id).await?;
102
103        // Renew token if it is valid for less than one week
104        let token = match claims.exp - clock::now() {
105            x if x < ONE_WEEK_IN_SECONDS => self.json_web_token.sign(user_compact.clone()).await,
106            _ => token.to_string(),
107        };
108
109        Ok((token, user_compact))
110    }
111}
112
113pub struct JsonWebToken {
114    cfg: Arc<Configuration>,
115}
116
117impl JsonWebToken {
118    pub fn new(cfg: Arc<Configuration>) -> Self {
119        Self { cfg }
120    }
121
122    /// Create Json Web Token.
123    ///
124    /// # Panics
125    ///
126    /// This function will panic if the default encoding algorithm does not รง
127    /// match the encoding key.
128    pub async fn sign(&self, user: UserCompact) -> String {
129        let settings = self.cfg.settings.read().await;
130
131        // Create JWT that expires in two weeks
132        let key = settings.auth.secret_key.as_bytes();
133
134        // todo: create config option for setting the token validity in seconds.
135        let exp_date = clock::now() + 1_209_600; // two weeks from now
136
137        let claims = UserClaims { user, exp: exp_date };
138
139        encode(&Header::default(), &claims, &EncodingKey::from_secret(key)).expect("argument `Header` should match `EncodingKey`")
140    }
141
142    /// Verify Json Web Token.
143    ///
144    /// # Errors
145    ///
146    /// This function will return an error if the JWT is not good or expired.
147    pub async fn verify(&self, token: &str) -> Result<UserClaims, ServiceError> {
148        let settings = self.cfg.settings.read().await;
149
150        match decode::<UserClaims>(
151            token,
152            &DecodingKey::from_secret(settings.auth.secret_key.as_bytes()),
153            &Validation::new(Algorithm::HS256),
154        ) {
155            Ok(token_data) => {
156                if token_data.claims.exp < clock::now() {
157                    return Err(ServiceError::TokenExpired);
158                }
159                Ok(token_data.claims)
160            }
161            Err(_) => Err(ServiceError::TokenInvalid),
162        }
163    }
164}
165
166pub struct DbUserAuthenticationRepository {
167    database: Arc<Box<dyn Database>>,
168}
169
170impl DbUserAuthenticationRepository {
171    #[must_use]
172    pub fn new(database: Arc<Box<dyn Database>>) -> Self {
173        Self { database }
174    }
175
176    /// Get user authentication data from user id.
177    ///
178    /// # Errors
179    ///
180    /// This function will return an error if unable to get the user
181    /// authentication data from the database.
182    pub async fn get_user_authentication_from_id(&self, user_id: &UserId) -> Result<UserAuthentication, Error> {
183        self.database.get_user_authentication_from_id(*user_id).await
184    }
185}
186
187/// Verify if the user supplied and the database supplied passwords match
188///
189/// # Errors
190///
191/// This function will return an error if unable to parse password hash from the stored user authentication value.
192/// This function will return a `ServiceError::WrongPasswordOrUsername` if unable to match the password with either `argon2id` or `pbkdf2-sha256`.
193fn verify_password(password: &[u8], user_authentication: &UserAuthentication) -> Result<(), ServiceError> {
194    // wrap string of the hashed password into a PasswordHash struct for verification
195    let parsed_hash = PasswordHash::new(&user_authentication.password_hash)?;
196
197    match parsed_hash.algorithm.as_str() {
198        "argon2id" => {
199            if Argon2::default().verify_password(password, &parsed_hash).is_err() {
200                return Err(ServiceError::WrongPasswordOrUsername);
201            }
202
203            Ok(())
204        }
205        "pbkdf2-sha256" => {
206            if Pbkdf2.verify_password(password, &parsed_hash).is_err() {
207                return Err(ServiceError::WrongPasswordOrUsername);
208            }
209
210            Ok(())
211        }
212        _ => Err(ServiceError::WrongPasswordOrUsername),
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::verify_password;
219    use crate::models::user::UserAuthentication;
220
221    #[test]
222    fn password_hashed_with_pbkdf2_sha256_should_be_verified() {
223        let password = "12345678".as_bytes();
224        let password_hash =
225            "$pbkdf2-sha256$i=10000,l=32$pZIh8nilm+cg6fk5Ubf2zQ$AngLuZ+sGUragqm4bIae/W+ior0TWxYFFaTx8CulqtY".to_string();
226        let user_authentication = UserAuthentication {
227            user_id: 1i64,
228            password_hash,
229        };
230
231        assert!(verify_password(password, &user_authentication).is_ok());
232        assert!(verify_password("incorrect password".as_bytes(), &user_authentication).is_err());
233    }
234
235    #[test]
236    fn password_hashed_with_argon2_should_be_verified() {
237        let password = "87654321".as_bytes();
238        let password_hash =
239            "$argon2id$v=19$m=4096,t=3,p=1$ycK5lJ4xmFBnaJ51M1j1eA$kU3UlNiSc3JDbl48TCj7JBDKmrT92DOUAgo4Yq0+nMw".to_string();
240        let user_authentication = UserAuthentication {
241            user_id: 1i64,
242            password_hash,
243        };
244
245        assert!(verify_password(password, &user_authentication).is_ok());
246        assert!(verify_password("incorrect password".as_bytes(), &user_authentication).is_err());
247    }
248}