Skip to main content

reifydb_auth/service/
authenticate.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::collections::HashMap;
5
6use reifydb_catalog::catalog::Catalog;
7use reifydb_core::interface::{
8	auth::{AuthStep, AuthenticationProvider},
9	catalog::{authentication::Authentication, identity::Identity},
10};
11use reifydb_transaction::transaction::{Transaction, query::QueryTransaction};
12use reifydb_value::{error::Error, reifydb_assertions, value::identity::IdentityId};
13use tracing::instrument;
14
15use super::{AuthResponse, AuthService, generate_session_token};
16use crate::{challenge::ChallengeInfo, error::AuthError};
17
18impl AuthService {
19	#[instrument(name = "auth::authenticate", level = "debug", skip(self, credentials))]
20	pub fn authenticate(&self, method: &str, credentials: HashMap<String, String>) -> Result<AuthResponse, Error> {
21		if let Some(challenge_id) = credentials.get("challenge_id").cloned() {
22			return self.authenticate_challenge_response(&challenge_id, credentials);
23		}
24		if method == "token" {
25			return self.authenticate_token(credentials);
26		}
27		self.authenticate_with_provider(method, credentials)
28	}
29
30	fn authenticate_with_provider(
31		&self,
32		method: &str,
33		credentials: HashMap<String, String>,
34	) -> Result<AuthResponse, Error> {
35		let identifier = credentials.get("identifier").map(|s| s.as_str()).unwrap_or("");
36		let mut txn = self.engine.begin_query()?;
37		let catalog = self.engine.catalog();
38
39		let Some(ident) = self.resolve_provider_identity(&mut txn, &catalog, method, identifier)? else {
40			drop(txn);
41			return self.handle_missing_identity(method, identifier, &credentials);
42		};
43		if !ident.enabled {
44			return Ok(AuthResponse::Failed {
45				reason: "identity is disabled".to_string(),
46			});
47		}
48
49		let Some(stored_auth) = self.load_stored_auth(&mut txn, &catalog, ident.id, method)? else {
50			return Ok(invalid_credentials());
51		};
52
53		self.run_provider_and_respond(&stored_auth, &credentials, ident.id, identifier, method)
54	}
55
56	#[inline]
57	fn resolve_provider_identity(
58		&self,
59		txn: &mut QueryTransaction,
60		catalog: &Catalog,
61		method: &str,
62		identifier: &str,
63	) -> Result<Option<Identity>, Error> {
64		if let Some(u) = catalog.find_identity_by_name(&mut Transaction::Query(txn), identifier)? {
65			return Ok(Some(u));
66		}
67		if method == "solana" {
68			return catalog.find_identity_by_solana_pubkey(&mut Transaction::Query(txn), identifier);
69		}
70		Ok(None)
71	}
72
73	fn load_stored_auth(
74		&self,
75		txn: &mut QueryTransaction,
76		catalog: &Catalog,
77		identity: IdentityId,
78		method: &str,
79	) -> Result<Option<Authentication>, Error> {
80		catalog.find_authentication_by_identity_and_method(&mut Transaction::Query(txn), identity, method)
81	}
82
83	#[inline]
84	fn run_provider_and_respond(
85		&self,
86		stored_auth: &Authentication,
87		credentials: &HashMap<String, String>,
88		identity: IdentityId,
89		identifier: &str,
90		method: &str,
91	) -> Result<AuthResponse, Error> {
92		let provider = self.provider_for(method)?;
93		let step = provider.authenticate(&stored_auth.properties, credentials)?;
94		self.respond_to_initial_auth_step(step, identity, identifier, method)
95	}
96
97	#[inline]
98	fn handle_missing_identity(
99		&self,
100		method: &str,
101		identifier: &str,
102		credentials: &HashMap<String, String>,
103	) -> Result<AuthResponse, Error> {
104		if method == "solana"
105			&& let Some(public_key) = credentials.get("public_key").cloned()
106		{
107			return self.auto_provision_solana(identifier, &public_key, credentials);
108		}
109		Ok(invalid_credentials())
110	}
111
112	#[inline]
113	fn respond_to_initial_auth_step(
114		&self,
115		step: AuthStep,
116		identity: IdentityId,
117		identifier: &str,
118		method: &str,
119	) -> Result<AuthResponse, Error> {
120		match step {
121			AuthStep::Authenticated => self.finalize_authentication(identity),
122			AuthStep::Failed => Ok(invalid_credentials()),
123			AuthStep::Challenge {
124				payload,
125			} => Ok(self.issue_challenge(identifier, method, payload)),
126		}
127	}
128
129	#[inline]
130	fn finalize_authentication(&self, identity: IdentityId) -> Result<AuthResponse, Error> {
131		reifydb_assertions! {
132			assert!(
133				identity != IdentityId::default(),
134				"authentication finalized for the nil placeholder identity instead of a resolved one, so an unauthenticated principal would receive a valid session token and gain authorization (identity={:?})",
135				identity
136			);
137		}
138		let token = generate_session_token(&self.rng);
139		self.persist_token(&token, identity)?;
140		Ok(AuthResponse::Authenticated {
141			identity,
142			token,
143		})
144	}
145
146	#[inline]
147	fn issue_challenge(&self, identifier: &str, method: &str, payload: HashMap<String, String>) -> AuthResponse {
148		let challenge_id = self.challenges.create(
149			identifier.to_string(),
150			method.to_string(),
151			payload.clone(),
152			&self.clock,
153			&self.rng,
154		);
155		AuthResponse::Challenge {
156			challenge_id,
157			payload,
158		}
159	}
160
161	#[inline]
162	fn provider_for(&self, method: &str) -> Result<&dyn AuthenticationProvider, Error> {
163		self.auth_registry.get(method).ok_or_else(|| {
164			Error::from(AuthError::UnknownMethod {
165				method: method.to_string(),
166			})
167		})
168	}
169
170	fn authenticate_token(&self, credentials: HashMap<String, String>) -> Result<AuthResponse, Error> {
171		let token_value = match credentials.get("token") {
172			Some(t) if !t.is_empty() => t,
173			_ => return Ok(invalid_credentials()),
174		};
175
176		match self.validate_token(token_value) {
177			Some(token) => self.finalize_authentication(token.identity),
178			None => Ok(invalid_credentials()),
179		}
180	}
181
182	fn authenticate_challenge_response(
183		&self,
184		challenge_id: &str,
185		mut credentials: HashMap<String, String>,
186	) -> Result<AuthResponse, Error> {
187		let Some(challenge) = self.consume_challenge(challenge_id, &mut credentials) else {
188			return Ok(AuthResponse::Failed {
189				reason: "invalid or expired challenge".to_string(),
190			});
191		};
192
193		let mut txn = self.engine.begin_query()?;
194		let catalog = self.engine.catalog();
195
196		let Some(ident) =
197			self.resolve_challenge_identity(&mut txn, &catalog, &challenge.identifier, &challenge.method)?
198		else {
199			return Ok(invalid_credentials());
200		};
201
202		let Some(stored_auth) = self.load_stored_auth(&mut txn, &catalog, ident.id, &challenge.method)? else {
203			return Ok(invalid_credentials());
204		};
205
206		self.run_challenge_provider_and_respond(&stored_auth, &credentials, ident.id, &challenge.method)
207	}
208
209	#[inline]
210	fn consume_challenge(
211		&self,
212		challenge_id: &str,
213		credentials: &mut HashMap<String, String>,
214	) -> Option<ChallengeInfo> {
215		let challenge = self.challenges.consume(challenge_id)?;
216		merge_challenge_payload(credentials, &challenge.payload);
217		Some(challenge)
218	}
219
220	#[inline]
221	fn resolve_challenge_identity(
222		&self,
223		txn: &mut QueryTransaction,
224		catalog: &Catalog,
225		identifier: &str,
226		method: &str,
227	) -> Result<Option<Identity>, Error> {
228		let resolved = match catalog.find_identity_by_name(&mut Transaction::Query(txn), identifier)? {
229			Some(u) if u.enabled => Some(u),
230			Some(_) => None,
231			None if method == "solana" => {
232				match catalog
233					.find_identity_by_solana_pubkey(&mut Transaction::Query(txn), identifier)?
234				{
235					Some(u) if u.enabled => Some(u),
236					_ => None,
237				}
238			}
239			None => None,
240		};
241		reifydb_assertions! {
242			if let Some(ref ident) = resolved {
243				assert!(
244					ident.enabled,
245					"challenge identity resolution returned a disabled identity (id={:?}, name={}); a disabled principal must never advance to provider authentication or it could obtain a session token",
246					ident.id,
247					ident.name
248				);
249			}
250		}
251		Ok(resolved)
252	}
253
254	#[inline]
255	fn run_challenge_provider_and_respond(
256		&self,
257		stored_auth: &Authentication,
258		credentials: &HashMap<String, String>,
259		identity: IdentityId,
260		method: &str,
261	) -> Result<AuthResponse, Error> {
262		let provider = self.provider_for(method)?;
263		let step = provider.authenticate(&stored_auth.properties, credentials)?;
264		respond_to_challenge_step(step, identity, self)
265	}
266}
267
268#[inline]
269fn merge_challenge_payload(credentials: &mut HashMap<String, String>, payload: &HashMap<String, String>) {
270	for (k, v) in payload {
271		credentials.entry(k.clone()).or_insert_with(|| v.clone());
272	}
273	credentials.remove("challenge_id");
274}
275
276#[inline]
277fn respond_to_challenge_step(
278	step: AuthStep,
279	identity: IdentityId,
280	service: &AuthService,
281) -> Result<AuthResponse, Error> {
282	match step {
283		AuthStep::Authenticated => service.finalize_authentication(identity),
284		AuthStep::Failed => Ok(invalid_credentials()),
285		AuthStep::Challenge {
286			..
287		} => Ok(AuthResponse::Failed {
288			reason: "nested challenges are not supported".to_string(),
289		}),
290	}
291}
292
293#[inline]
294fn invalid_credentials() -> AuthResponse {
295	AuthResponse::Failed {
296		reason: "invalid credentials".to_string(),
297	}
298}