Skip to main content

reifydb_auth/method/
password.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::collections::HashMap;
5
6use argon2::{
7	Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version,
8	password_hash::Error as PasswordHashError,
9};
10use reifydb_core::interface::auth::{AuthStep, AuthenticationProvider};
11use reifydb_runtime::context::rng::Rng;
12use reifydb_type::{Result, error::Error};
13
14use crate::error::AuthError;
15
16pub struct PasswordProvider;
17
18/// OWASP-recommended Argon2id parameters:
19/// 19 MiB memory, 2 iterations, parallelism 1, 32-byte output.
20fn argon2_instance() -> Argon2<'static> {
21	let params = Params::new(19 * 1024, 2, 1, Some(32)).expect("valid Argon2 params");
22	Argon2::new(Algorithm::Argon2id, Version::V0x13, params)
23}
24
25impl AuthenticationProvider for PasswordProvider {
26	fn method(&self) -> &str {
27		"password"
28	}
29
30	fn create(&self, _rng: &Rng, config: &HashMap<String, String>) -> Result<HashMap<String, String>> {
31		let password = config.get("password").ok_or_else(|| Error::from(AuthError::PasswordRequired))?;
32
33		let argon2 = argon2_instance();
34
35		let phc = argon2
36			.hash_password(password.as_bytes())
37			.map_err(|e| {
38				Error::from(AuthError::HashingFailed {
39					reason: e.to_string(),
40				})
41			})?
42			.to_string();
43
44		Ok(HashMap::from([("phc".into(), phc), ("algorithm_version".into(), "1".into())]))
45	}
46
47	fn authenticate(
48		&self,
49		stored: &HashMap<String, String>,
50		credentials: &HashMap<String, String>,
51	) -> Result<AuthStep> {
52		let credential = credentials.get("password").ok_or_else(|| Error::from(AuthError::PasswordRequired))?;
53
54		let phc_str = stored.get("phc").ok_or_else(|| {
55			Error::from(AuthError::InvalidHash {
56				reason: "missing 'phc' field".to_string(),
57			})
58		})?;
59
60		let parsed_hash = PasswordHash::new(phc_str).map_err(|e| {
61			Error::from(AuthError::InvalidHash {
62				reason: e.to_string(),
63			})
64		})?;
65
66		let argon2 = argon2_instance();
67
68		match argon2.verify_password(credential.as_bytes(), &parsed_hash) {
69			Ok(()) => Ok(AuthStep::Authenticated),
70			Err(PasswordHashError::PasswordInvalid) => Ok(AuthStep::Failed),
71			Err(e) => Err(Error::from(AuthError::VerificationFailed {
72				reason: e.to_string(),
73			})),
74		}
75	}
76}
77
78#[cfg(test)]
79mod tests {
80	use super::*;
81
82	#[test]
83	fn test_password_create_and_authenticate() {
84		let provider = PasswordProvider;
85		let config = HashMap::from([("password".to_string(), "secret123".to_string())]);
86
87		let stored = provider.create(&Rng::default(), &config).unwrap();
88		assert!(stored.contains_key("phc"));
89		assert!(stored.get("phc").unwrap().starts_with("$argon2id$"));
90		assert_eq!(stored.get("algorithm_version").unwrap(), "1");
91
92		let correct = HashMap::from([("password".to_string(), "secret123".to_string())]);
93		assert_eq!(provider.authenticate(&stored, &correct).unwrap(), AuthStep::Authenticated);
94
95		let wrong = HashMap::from([("password".to_string(), "wrong_password".to_string())]);
96		assert_eq!(provider.authenticate(&stored, &wrong).unwrap(), AuthStep::Failed);
97	}
98
99	#[test]
100	fn test_password_requires_password_field() {
101		let provider = PasswordProvider;
102		let config = HashMap::new();
103		assert!(provider.create(&Rng::default(), &config).is_err());
104	}
105
106	#[test]
107	fn test_authenticate_corrupted_hash() {
108		let provider = PasswordProvider;
109		let stored = HashMap::from([
110			("phc".into(), "not-a-valid-phc-string".to_string()),
111			("algorithm_version".into(), "1".into()),
112		]);
113		let creds = HashMap::from([("password".to_string(), "anything".to_string())]);
114		assert!(provider.authenticate(&stored, &creds).is_err());
115	}
116
117	#[test]
118	fn test_authenticate_missing_phc() {
119		let provider = PasswordProvider;
120		let stored = HashMap::new();
121		let creds = HashMap::from([("password".to_string(), "anything".to_string())]);
122		assert!(provider.authenticate(&stored, &creds).is_err());
123	}
124
125	#[test]
126	fn test_authenticate_missing_password_credential() {
127		let provider = PasswordProvider;
128		let config = HashMap::from([("password".to_string(), "secret123".to_string())]);
129		let stored = provider.create(&Rng::default(), &config).unwrap();
130		let empty_creds = HashMap::new();
131		assert!(provider.authenticate(&stored, &empty_creds).is_err());
132	}
133}