Skip to main content

secrets_auth_userpass/
lib.rs

1use async_trait::async_trait;
2use argon2::password_hash::rand_core::OsRng;
3use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
4use argon2::Argon2;
5use secrets_core::auth::{AuthError, AuthMethod, AuthOutcome, AuthResult, LoginRequest};
6use secrets_core::storage::{StorageBackend, StorageEntry};
7use serde::{Deserialize, Serialize};
8
9const USER_PREFIX: &str = "auth/userpass/users/";
10const DEFAULT_TTL_SECONDS: i64 = 3600;
11
12#[derive(Debug, Serialize, Deserialize)]
13struct UserRecord {
14    password_hash: String,
15    policies: Vec<String>,
16}
17
18pub struct UserPassAuth;
19
20impl Default for UserPassAuth {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl UserPassAuth {
27    pub fn new() -> Self {
28        Self
29    }
30
31    pub fn hash_password(password: &str) -> AuthResult<String> {
32        let salt = SaltString::generate(&mut OsRng);
33        Argon2::default()
34            .hash_password(password.as_bytes(), &salt)
35            .map(|h| h.to_string())
36            .map_err(|e| AuthError::Other(e.to_string()))
37    }
38
39    pub async fn user_exists(storage: &dyn StorageBackend, username: &str) -> AuthResult<bool> {
40        Ok(storage.get(&format!("{USER_PREFIX}{username}")).await?.is_some())
41    }
42
43    pub async fn create_user(
44        storage: &dyn StorageBackend,
45        username: &str,
46        password: &str,
47        policies: Vec<String>,
48    ) -> AuthResult<()> {
49        let record = UserRecord {
50            password_hash: Self::hash_password(password)?,
51            policies,
52        };
53        let value = serde_json::to_vec(&record).map_err(|e| AuthError::Other(e.to_string()))?;
54        storage
55            .put(
56                &format!("{USER_PREFIX}{username}"),
57                StorageEntry {
58                    value,
59                    expires_at: None,
60                },
61            )
62            .await?;
63        Ok(())
64    }
65}
66
67#[async_trait]
68impl AuthMethod for UserPassAuth {
69    async fn login(
70        &self,
71        storage: &dyn StorageBackend,
72        request: LoginRequest,
73    ) -> AuthResult<AuthOutcome> {
74        let LoginRequest::UserPass { username, password } = request else {
75            return Err(AuthError::InvalidRequest(
76                "expected username/password".into(),
77            ));
78        };
79
80        let entry = storage
81            .get(&format!("{USER_PREFIX}{username}"))
82            .await?
83            .ok_or(AuthError::InvalidCredentials)?;
84        let record: UserRecord = serde_json::from_slice(&entry.value)
85            .map_err(|e| AuthError::Other(e.to_string()))?;
86
87        let hash = PasswordHash::new(&record.password_hash)
88            .map_err(|e| AuthError::Other(e.to_string()))?;
89        Argon2::default()
90            .verify_password(password.as_bytes(), &hash)
91            .map_err(|_| AuthError::InvalidCredentials)?;
92
93        Ok(AuthOutcome {
94            policies: record.policies,
95            display_name: username,
96            ttl_seconds: Some(DEFAULT_TTL_SECONDS),
97        })
98    }
99}