secrets_auth_userpass/
lib.rs1use 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}