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::{
13 error::Error,
14 reifydb_assertions,
15 value::{Value, identity::IdentityId},
16};
17use tracing::instrument;
18
19use super::{AuthResponse, AuthService, generate_session_token, solana::SOLANA_PUBLIC_KEY_ATTRIBUTE};
20use crate::error::AuthError;
21
22impl AuthService {
23 #[instrument(name = "auth::authenticate", level = "debug", skip(self, credentials))]
24 pub fn authenticate(&self, method: &str, credentials: HashMap<String, String>) -> Result<AuthResponse, Error> {
25 if let Some(challenge_id) = credentials.get("challenge_id").cloned() {
26 return self.authenticate_challenge_response(method, &challenge_id, credentials);
27 }
28 if method == "token" {
29 return self.authenticate_token(credentials);
30 }
31 if method == "github" {
32 return self.begin_github_login();
33 }
34 self.authenticate_with_provider(method, credentials)
35 }
36
37 fn authenticate_with_provider(
38 &self,
39 method: &str,
40 credentials: HashMap<String, String>,
41 ) -> Result<AuthResponse, Error> {
42 let identifier = credentials.get("identifier").map(|s| s.as_str()).unwrap_or("");
43 let mut txn = self.engine.begin_query()?;
44 let catalog = self.engine.catalog();
45
46 let Some(ident) = self.resolve_provider_identity(&mut txn, &catalog, method, identifier)? else {
47 drop(txn);
48 return self.handle_missing_identity(method, identifier, &credentials);
49 };
50 if !ident.enabled {
51 return Ok(AuthResponse::Failed {
52 reason: "identity is disabled".to_string(),
53 });
54 }
55
56 let Some(stored_auth) = self.load_stored_auth(&mut txn, &catalog, ident.id, method)? else {
57 return Ok(invalid_credentials());
58 };
59
60 self.run_provider_and_respond(&stored_auth, &credentials, ident.id, identifier, method)
61 }
62
63 #[inline]
64 fn resolve_provider_identity(
65 &self,
66 txn: &mut QueryTransaction,
67 catalog: &Catalog,
68 method: &str,
69 identifier: &str,
70 ) -> Result<Option<Identity>, Error> {
71 if let Some(u) = catalog.find_identity_by_name(&mut Transaction::Query(txn), identifier)? {
72 return Ok(Some(u));
73 }
74 if method == "solana" {
75 return catalog.find_identity_by_attribute_value(
76 &mut Transaction::Query(txn),
77 SOLANA_PUBLIC_KEY_ATTRIBUTE,
78 &Value::Utf8(identifier.to_string()),
79 );
80 }
81 Ok(None)
82 }
83
84 fn load_stored_auth(
85 &self,
86 txn: &mut QueryTransaction,
87 catalog: &Catalog,
88 identity: IdentityId,
89 method: &str,
90 ) -> Result<Option<Authentication>, Error> {
91 catalog.find_authentication_by_identity_and_method(&mut Transaction::Query(txn), identity, method)
92 }
93
94 #[inline]
95 fn run_provider_and_respond(
96 &self,
97 stored_auth: &Authentication,
98 credentials: &HashMap<String, String>,
99 identity: IdentityId,
100 identifier: &str,
101 method: &str,
102 ) -> Result<AuthResponse, Error> {
103 let provider = self.provider_for(method)?;
104 let step = provider.authenticate(&stored_auth.properties, credentials)?;
105 self.respond_to_initial_auth_step(step, identity, identifier, method)
106 }
107
108 #[inline]
109 fn handle_missing_identity(
110 &self,
111 method: &str,
112 identifier: &str,
113 credentials: &HashMap<String, String>,
114 ) -> Result<AuthResponse, Error> {
115 if method == "solana"
116 && let Some(public_key) = credentials.get("public_key").cloned()
117 {
118 return self.begin_solana_provision(identifier, &public_key, credentials);
119 }
120 Ok(invalid_credentials())
121 }
122
123 #[inline]
124 fn respond_to_initial_auth_step(
125 &self,
126 step: AuthStep,
127 identity: IdentityId,
128 identifier: &str,
129 method: &str,
130 ) -> Result<AuthResponse, Error> {
131 match step {
132 AuthStep::Authenticated => self.finalize_authentication(identity),
133 AuthStep::Failed => Ok(invalid_credentials()),
134 AuthStep::Rejected {
135 reason,
136 } => Ok(AuthResponse::Failed {
137 reason,
138 }),
139 AuthStep::Challenge {
140 payload,
141 } => Ok(self.issue_challenge(identifier, method, payload)),
142 }
143 }
144
145 #[inline]
146 pub(super) fn finalize_authentication(&self, identity: IdentityId) -> Result<AuthResponse, Error> {
147 reifydb_assertions! {
148 assert!(
149 identity != IdentityId::default(),
150 "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={:?})",
151 identity
152 );
153 }
154 let token = generate_session_token(&self.rng);
155 self.persist_token(&token, identity)?;
156 Ok(AuthResponse::Authenticated {
157 identity,
158 token,
159 })
160 }
161
162 #[inline]
163 fn issue_challenge(&self, identifier: &str, method: &str, payload: HashMap<String, String>) -> AuthResponse {
164 let challenge_id = self.challenges.create(
165 identifier.to_string(),
166 method.to_string(),
167 payload.clone(),
168 None,
169 &self.clock,
170 &self.rng,
171 );
172 AuthResponse::Challenge {
173 challenge_id,
174 payload,
175 }
176 }
177
178 #[inline]
179 fn provider_for(&self, method: &str) -> Result<&dyn AuthenticationProvider, Error> {
180 self.auth_registry.get(method).ok_or_else(|| {
181 Error::from(AuthError::UnknownMethod {
182 method: method.to_string(),
183 })
184 })
185 }
186
187 fn authenticate_token(&self, credentials: HashMap<String, String>) -> Result<AuthResponse, Error> {
188 let token_value = match credentials.get("token") {
189 Some(t) if !t.is_empty() => t,
190 _ => return Ok(invalid_credentials()),
191 };
192
193 match self.validate_token(token_value)? {
194 Some(token) => self.finalize_authentication(token.identity),
195 None => Ok(invalid_credentials()),
196 }
197 }
198
199 fn authenticate_challenge_response(
200 &self,
201 method: &str,
202 challenge_id: &str,
203 mut credentials: HashMap<String, String>,
204 ) -> Result<AuthResponse, Error> {
205 let Some(challenge) = self.challenges.consume(challenge_id) else {
206 return Ok(AuthResponse::Failed {
207 reason: "invalid or expired challenge".to_string(),
208 });
209 };
210
211 if method != challenge.method {
212 return Ok(AuthResponse::Failed {
213 reason: "challenge was issued for a different method".to_string(),
214 });
215 }
216
217 if challenge.method == "github" {
218 return self.complete_github_login(&challenge, &credentials);
219 }
220
221 if let Some(public_key) = challenge.pending_public_key.as_deref() {
222 return self.complete_solana_provision(
223 &challenge.identifier,
224 public_key,
225 &challenge.payload,
226 &credentials,
227 );
228 }
229
230 merge_challenge_payload(&mut credentials, &challenge.payload);
231
232 let mut txn = self.engine.begin_query()?;
233 let catalog = self.engine.catalog();
234
235 let Some(ident) =
236 self.resolve_challenge_identity(&mut txn, &catalog, &challenge.identifier, &challenge.method)?
237 else {
238 return Ok(invalid_credentials());
239 };
240
241 let Some(stored_auth) = self.load_stored_auth(&mut txn, &catalog, ident.id, &challenge.method)? else {
242 return Ok(invalid_credentials());
243 };
244
245 self.run_challenge_provider_and_respond(
246 &stored_auth,
247 &challenge.payload,
248 &credentials,
249 ident.id,
250 &challenge.method,
251 )
252 }
253
254 #[inline]
255 fn resolve_challenge_identity(
256 &self,
257 txn: &mut QueryTransaction,
258 catalog: &Catalog,
259 identifier: &str,
260 method: &str,
261 ) -> Result<Option<Identity>, Error> {
262 let resolved = match catalog.find_identity_by_name(&mut Transaction::Query(txn), identifier)? {
263 Some(u) if u.enabled => Some(u),
264 Some(_) => None,
265 None if method == "solana" => {
266 match catalog.find_identity_by_attribute_value(
267 &mut Transaction::Query(txn),
268 SOLANA_PUBLIC_KEY_ATTRIBUTE,
269 &Value::Utf8(identifier.to_string()),
270 )? {
271 Some(u) if u.enabled => Some(u),
272 _ => None,
273 }
274 }
275 None => None,
276 };
277 reifydb_assertions! {
278 if let Some(ref ident) = resolved {
279 assert!(
280 ident.enabled,
281 "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",
282 ident.id,
283 ident.name
284 );
285 }
286 }
287 Ok(resolved)
288 }
289
290 #[inline]
291 fn run_challenge_provider_and_respond(
292 &self,
293 stored_auth: &Authentication,
294 challenge_payload: &HashMap<String, String>,
295 credentials: &HashMap<String, String>,
296 identity: IdentityId,
297 method: &str,
298 ) -> Result<AuthResponse, Error> {
299 let provider = self.provider_for(method)?;
300 let step = provider.verify_challenge(&stored_auth.properties, challenge_payload, credentials)?;
301 respond_to_challenge_step(step, identity, self)
302 }
303}
304
305#[inline]
306fn merge_challenge_payload(credentials: &mut HashMap<String, String>, payload: &HashMap<String, String>) {
307 for (k, v) in payload {
308 credentials.insert(k.clone(), v.clone());
309 }
310 credentials.remove("challenge_id");
311}
312
313#[inline]
314fn respond_to_challenge_step(
315 step: AuthStep,
316 identity: IdentityId,
317 service: &AuthService,
318) -> Result<AuthResponse, Error> {
319 match step {
320 AuthStep::Authenticated => service.finalize_authentication(identity),
321 AuthStep::Failed => Ok(invalid_credentials()),
322 AuthStep::Rejected {
323 reason,
324 } => Ok(AuthResponse::Failed {
325 reason,
326 }),
327 AuthStep::Challenge {
328 ..
329 } => Ok(AuthResponse::Failed {
330 reason: "nested challenges are not supported".to_string(),
331 }),
332 }
333}
334
335#[inline]
336fn invalid_credentials() -> AuthResponse {
337 AuthResponse::Failed {
338 reason: "invalid credentials".to_string(),
339 }
340}