1use 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}