cosmodrome/server/
passport.rs

1//! Passports are the identification card for a user. Traditionally known as `Account`.
2use anyhow::anyhow;
3use argon2::{
4    password_hash::{
5        rand_core::OsRng,
6        Encoding,
7        PasswordHash,
8        PasswordHasher,
9        PasswordVerifier,
10        SaltString,
11    },
12    Argon2,
13};
14use chrono::{
15    DateTime,
16    TimeDelta,
17    Utc,
18};
19pub use passport_type::PassportType;
20use rocket::serde::{
21    Deserialize,
22    Serialize,
23};
24
25mod passport_type;
26
27/// Defines a passport of a user.
28#[derive(Serialize, Deserialize, Clone, Debug)]
29#[serde(crate = "rocket::serde")]
30pub struct Passport {
31    /// The unique id of the passport. For example username, email or some string of your choice.
32    pub id: String,
33    /// Password to login to the service. Resides encoded in memory.
34    password: String,
35    /// A list of scopes that the user can access.
36    services: Vec<String>,
37    /// Type of this passport.
38    pub account_type: PassportType,
39    /// Wether the passport is disabled.
40    pub disabled: bool,
41    /// Whether the passport has been confirmed. This is useful in combination
42    /// with for example E-Mail verficiation.
43    pub confirmed: bool,
44    /// Determines when this passport expires.
45    pub expires_at: DateTime<Utc>,
46}
47
48impl Passport {
49    /// Creates a new passport with [Passport::disabled] and [Passport::confirmed] set to `false`.
50    pub fn new(
51        id: &str,
52        password: &str,
53        services: &[&str],
54        account_type: PassportType,
55    ) -> anyhow::Result<Self> {
56        Ok(Self {
57            id: id.to_string(),
58            password: Self::hash_password(password)?,
59            services: services
60                .iter()
61                .map(|s| s.to_string())
62                .collect::<Vec<String>>(),
63            account_type,
64            disabled: false,  // always activate
65            confirmed: false, // always require user to confirm it
66            expires_at: chrono::Utc::now()
67                + TimeDelta::try_weeks(104).ok_or(anyhow!(
68                    "Internal server error. Could not create TimeDelta with \
69                     two years."
70                ))?,
71        })
72    }
73
74    /// Returns the services this passport is valid for.
75    pub fn services(&self) -> &[String] {
76        &self.services
77    }
78
79    /// Saves the ```new_password``` to the struct after verifying the ```old_password```.
80    /// Does NOT automatically call the ```update``` function to update the database.
81    pub fn change_password(
82        &mut self,
83        old_password: &str,
84        new_password: &str,
85    ) -> anyhow::Result<()> {
86        if self.verify_password(old_password)? {
87            self.password = Self::hash_password(new_password)?;
88            Ok(())
89        } else {
90            Err(anyhow!("Passwords do not match."))
91        }
92    }
93
94    /// Checks if the given password is correct.
95    pub fn verify_password(&self, password: &str) -> anyhow::Result<bool> {
96        let hash = PasswordHash::parse(&self.password, Encoding::B64)
97            .map_err(|e| anyhow!("{e}"))?;
98        Ok(Argon2::default()
99            .verify_password(password.as_bytes(), &hash)
100            .is_ok())
101    }
102
103    /// Hashes the password using `[argon2]`.
104    fn hash_password(password: &str) -> anyhow::Result<String> {
105        let salt = SaltString::generate(&mut OsRng);
106        let argon2 = Argon2::default();
107        Ok(argon2
108            .hash_password(password.as_bytes(), &salt)
109            .map_err(|e| anyhow!("{e}"))?
110            .to_string())
111    }
112}