Skip to main content

reifydb_auth/method/
password.rs

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