Skip to main content

reifydb_auth/service/
authenticate.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::collections::HashMap;
5
6use reifydb_core::interface::auth::AuthStep;
7use reifydb_transaction::transaction::Transaction;
8use reifydb_type::error::Error;
9use tracing::instrument;
10
11use super::{AuthResponse, AuthService, generate_session_token};
12use crate::error::AuthError;
13
14impl AuthService {
15	/// Authenticate an identity with the given method and credentials.
16	///
17	/// For single-step methods (password, token), returns `Authenticated` or `Failed`.
18	/// For challenge-response methods, may return `Challenge` first, then `Authenticated`
19	/// on the second call with the challenge response.
20	///
21	/// For Solana authentication, if the identity does not exist and the credentials contain
22	/// a `public_key`, the identity and authentication method are auto-provisioned before
23	/// proceeding with the challenge-response flow.
24	#[instrument(name = "auth::authenticate", level = "debug", skip(self, credentials))]
25	pub fn authenticate(&self, method: &str, credentials: HashMap<String, String>) -> Result<AuthResponse, Error> {
26		if let Some(challenge_id) = credentials.get("challenge_id").cloned() {
27			return self.authenticate_challenge_response(&challenge_id, credentials);
28		}
29
30		if method == "token" {
31			return self.authenticate_token(credentials);
32		}
33
34		let identifier = credentials.get("identifier").map(|s| s.as_str()).unwrap_or("");
35
36		let mut txn = self.engine.begin_query()?;
37		let catalog = self.engine.catalog();
38
39		let ident = match catalog.find_identity_by_name(&mut Transaction::Query(&mut txn), identifier)? {
40			Some(u) => u,
41			None => {
42				drop(txn);
43
44				if method == "solana"
45					&& let Some(public_key) = credentials.get("public_key").cloned()
46				{
47					return self.auto_provision_solana(identifier, &public_key, &credentials);
48				}
49				return Ok(AuthResponse::Failed {
50					reason: "invalid credentials".to_string(),
51				});
52			}
53		};
54
55		if !ident.enabled {
56			return Ok(AuthResponse::Failed {
57				reason: "identity is disabled".to_string(),
58			});
59		}
60
61		let stored_auth = match catalog.find_authentication_by_identity_and_method(
62			&mut Transaction::Query(&mut txn),
63			ident.id,
64			method,
65		)? {
66			Some(a) => a,
67			None => {
68				return Ok(AuthResponse::Failed {
69					reason: "invalid credentials".to_string(),
70				});
71			}
72		};
73
74		let provider = self.auth_registry.get(method).ok_or_else(|| {
75			Error::from(AuthError::UnknownMethod {
76				method: method.to_string(),
77			})
78		})?;
79
80		match provider.authenticate(&stored_auth.properties, &credentials)? {
81			AuthStep::Authenticated => {
82				let token = generate_session_token(&self.rng);
83				self.persist_token(&token, ident.id)?;
84				Ok(AuthResponse::Authenticated {
85					identity: ident.id,
86					token,
87				})
88			}
89			AuthStep::Failed => Ok(AuthResponse::Failed {
90				reason: "invalid credentials".to_string(),
91			}),
92			AuthStep::Challenge {
93				payload,
94			} => {
95				let challenge_id = self.challenges.create(
96					identifier.to_string(),
97					method.to_string(),
98					payload.clone(),
99					&self.clock,
100					&self.rng,
101				);
102				Ok(AuthResponse::Challenge {
103					challenge_id,
104					payload,
105				})
106			}
107		}
108	}
109
110	fn authenticate_token(&self, credentials: HashMap<String, String>) -> Result<AuthResponse, Error> {
111		let token_value = match credentials.get("token") {
112			Some(t) if !t.is_empty() => t,
113			_ => {
114				return Ok(AuthResponse::Failed {
115					reason: "invalid credentials".to_string(),
116				});
117			}
118		};
119
120		match self.validate_token(token_value) {
121			Some(token) => {
122				let session_token = generate_session_token(&self.rng);
123				self.persist_token(&session_token, token.identity)?;
124				Ok(AuthResponse::Authenticated {
125					identity: token.identity,
126					token: session_token,
127				})
128			}
129			None => Ok(AuthResponse::Failed {
130				reason: "invalid credentials".to_string(),
131			}),
132		}
133	}
134
135	/// Complete a challenge-response authentication flow.
136	fn authenticate_challenge_response(
137		&self,
138		challenge_id: &str,
139		mut credentials: HashMap<String, String>,
140	) -> Result<AuthResponse, Error> {
141		let challenge = match self.challenges.consume(challenge_id) {
142			Some(c) => c,
143			None => {
144				return Ok(AuthResponse::Failed {
145					reason: "invalid or expired challenge".to_string(),
146				});
147			}
148		};
149
150		// Merge challenge payload into credentials so the provider can verify
151		for (k, v) in &challenge.payload {
152			credentials.entry(k.clone()).or_insert_with(|| v.clone());
153		}
154
155		// Remove the challenge_id from credentials before passing to provider
156		credentials.remove("challenge_id");
157
158		// Look up identity and auth again (challenge may have been issued a while ago)
159		let mut txn = self.engine.begin_query()?;
160		let catalog = self.engine.catalog();
161
162		let ident = match catalog
163			.find_identity_by_name(&mut Transaction::Query(&mut txn), &challenge.identifier)?
164		{
165			Some(u) if u.enabled => u,
166			_ => {
167				return Ok(AuthResponse::Failed {
168					reason: "invalid credentials".to_string(),
169				});
170			}
171		};
172
173		let stored_auth = match catalog.find_authentication_by_identity_and_method(
174			&mut Transaction::Query(&mut txn),
175			ident.id,
176			&challenge.method,
177		)? {
178			Some(a) => a,
179			None => {
180				return Ok(AuthResponse::Failed {
181					reason: "invalid credentials".to_string(),
182				});
183			}
184		};
185
186		let provider = self.auth_registry.get(&challenge.method).ok_or_else(|| {
187			Error::from(AuthError::UnknownMethod {
188				method: challenge.method.clone(),
189			})
190		})?;
191
192		match provider.authenticate(&stored_auth.properties, &credentials)? {
193			AuthStep::Authenticated => {
194				let token = generate_session_token(&self.rng);
195				self.persist_token(&token, ident.id)?;
196				Ok(AuthResponse::Authenticated {
197					identity: ident.id,
198					token,
199				})
200			}
201			AuthStep::Failed => Ok(AuthResponse::Failed {
202				reason: "invalid credentials".to_string(),
203			}),
204			AuthStep::Challenge {
205				..
206			} => Ok(AuthResponse::Failed {
207				reason: "nested challenges are not supported".to_string(),
208			}),
209		}
210	}
211}