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					if let Some(public_key) = credentials.get("public_key").cloned() {
46						return self.auto_provision_solana(
47							identifier,
48							&public_key,
49							&credentials,
50						);
51					}
52				}
53				return Ok(AuthResponse::Failed {
54					reason: "invalid credentials".to_string(),
55				});
56			}
57		};
58
59		if !ident.enabled {
60			return Ok(AuthResponse::Failed {
61				reason: "identity is disabled".to_string(),
62			});
63		}
64
65		let stored_auth = match catalog.find_authentication_by_identity_and_method(
66			&mut Transaction::Query(&mut txn),
67			ident.id,
68			method,
69		)? {
70			Some(a) => a,
71			None => {
72				return Ok(AuthResponse::Failed {
73					reason: "invalid credentials".to_string(),
74				});
75			}
76		};
77
78		let provider = self.auth_registry.get(method).ok_or_else(|| {
79			Error::from(AuthError::UnknownMethod {
80				method: method.to_string(),
81			})
82		})?;
83
84		match provider.authenticate(&stored_auth.properties, &credentials)? {
85			AuthStep::Authenticated => {
86				let token = generate_session_token(&self.rng);
87				self.persist_token(&token, ident.id)?;
88				Ok(AuthResponse::Authenticated {
89					identity: ident.id,
90					token,
91				})
92			}
93			AuthStep::Failed => Ok(AuthResponse::Failed {
94				reason: "invalid credentials".to_string(),
95			}),
96			AuthStep::Challenge {
97				payload,
98			} => {
99				let challenge_id = self.challenges.create(
100					identifier.to_string(),
101					method.to_string(),
102					payload.clone(),
103				);
104				Ok(AuthResponse::Challenge {
105					challenge_id,
106					payload,
107				})
108			}
109		}
110	}
111
112	fn authenticate_token(&self, credentials: HashMap<String, String>) -> Result<AuthResponse, Error> {
113		let token_value = match credentials.get("token") {
114			Some(t) if !t.is_empty() => t,
115			_ => {
116				return Ok(AuthResponse::Failed {
117					reason: "invalid credentials".to_string(),
118				});
119			}
120		};
121
122		match self.validate_token(token_value) {
123			Some(token) => {
124				let session_token = generate_session_token(&self.rng);
125				self.persist_token(&session_token, token.identity)?;
126				Ok(AuthResponse::Authenticated {
127					identity: token.identity,
128					token: session_token,
129				})
130			}
131			None => Ok(AuthResponse::Failed {
132				reason: "invalid credentials".to_string(),
133			}),
134		}
135	}
136
137	/// Complete a challenge-response authentication flow.
138	fn authenticate_challenge_response(
139		&self,
140		challenge_id: &str,
141		mut credentials: HashMap<String, String>,
142	) -> Result<AuthResponse, Error> {
143		let challenge = match self.challenges.consume(challenge_id) {
144			Some(c) => c,
145			None => {
146				return Ok(AuthResponse::Failed {
147					reason: "invalid or expired challenge".to_string(),
148				});
149			}
150		};
151
152		// Merge challenge payload into credentials so the provider can verify
153		for (k, v) in &challenge.payload {
154			credentials.entry(k.clone()).or_insert_with(|| v.clone());
155		}
156
157		// Remove the challenge_id from credentials before passing to provider
158		credentials.remove("challenge_id");
159
160		// Look up identity and auth again (challenge may have been issued a while ago)
161		let mut txn = self.engine.begin_query()?;
162		let catalog = self.engine.catalog();
163
164		let ident = match catalog
165			.find_identity_by_name(&mut Transaction::Query(&mut txn), &challenge.identifier)?
166		{
167			Some(u) if u.enabled => u,
168			_ => {
169				return Ok(AuthResponse::Failed {
170					reason: "invalid credentials".to_string(),
171				});
172			}
173		};
174
175		let stored_auth = match catalog.find_authentication_by_identity_and_method(
176			&mut Transaction::Query(&mut txn),
177			ident.id,
178			&challenge.method,
179		)? {
180			Some(a) => a,
181			None => {
182				return Ok(AuthResponse::Failed {
183					reason: "invalid credentials".to_string(),
184				});
185			}
186		};
187
188		let provider = self.auth_registry.get(&challenge.method).ok_or_else(|| {
189			Error::from(AuthError::UnknownMethod {
190				method: challenge.method.clone(),
191			})
192		})?;
193
194		match provider.authenticate(&stored_auth.properties, &credentials)? {
195			AuthStep::Authenticated => {
196				let token = generate_session_token(&self.rng);
197				self.persist_token(&token, ident.id)?;
198				Ok(AuthResponse::Authenticated {
199					identity: ident.id,
200					token,
201				})
202			}
203			AuthStep::Failed => Ok(AuthResponse::Failed {
204				reason: "invalid credentials".to_string(),
205			}),
206			AuthStep::Challenge {
207				..
208			} => Ok(AuthResponse::Failed {
209				reason: "nested challenges are not supported".to_string(),
210			}),
211		}
212	}
213}