1use std::collections::HashMap;
5
6use reifydb_core::interface::auth::{AuthStep, AuthenticationProvider};
7use reifydb_transaction::transaction::Transaction;
8use reifydb_type::{error::Error, value::identity::IdentityId};
9use tracing::instrument;
10
11use super::{AuthResponse, AuthService, generate_session_token};
12use crate::error::AuthError;
13
14impl AuthService {
15 #[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 if method == "token" {
30 return self.authenticate_token(credentials);
31 }
32 self.authenticate_with_provider(method, credentials)
33 }
34
35 fn authenticate_with_provider(
36 &self,
37 method: &str,
38 credentials: HashMap<String, String>,
39 ) -> Result<AuthResponse, Error> {
40 let identifier = credentials.get("identifier").map(|s| s.as_str()).unwrap_or("");
41 let mut txn = self.engine.begin_query()?;
42 let catalog = self.engine.catalog();
43
44 let ident = match catalog.find_identity_by_name(&mut Transaction::Query(&mut txn), identifier)? {
45 Some(u) => u,
46 None => {
47 drop(txn);
48 return self.handle_missing_identity(method, identifier, &credentials);
49 }
50 };
51 if !ident.enabled {
52 return Ok(AuthResponse::Failed {
53 reason: "identity is disabled".to_string(),
54 });
55 }
56
57 let Some(stored_auth) = catalog.find_authentication_by_identity_and_method(
58 &mut Transaction::Query(&mut txn),
59 ident.id,
60 method,
61 )?
62 else {
63 return Ok(invalid_credentials());
64 };
65
66 let provider = self.provider_for(method)?;
67 let step = provider.authenticate(&stored_auth.properties, &credentials)?;
68 self.respond_to_initial_auth_step(step, ident.id, identifier, method)
69 }
70
71 #[inline]
72 fn handle_missing_identity(
73 &self,
74 method: &str,
75 identifier: &str,
76 credentials: &HashMap<String, String>,
77 ) -> Result<AuthResponse, Error> {
78 if method == "solana"
79 && let Some(public_key) = credentials.get("public_key").cloned()
80 {
81 return self.auto_provision_solana(identifier, &public_key, credentials);
82 }
83 Ok(invalid_credentials())
84 }
85
86 #[inline]
87 fn respond_to_initial_auth_step(
88 &self,
89 step: AuthStep,
90 identity: IdentityId,
91 identifier: &str,
92 method: &str,
93 ) -> Result<AuthResponse, Error> {
94 match step {
95 AuthStep::Authenticated => self.finalize_authentication(identity),
96 AuthStep::Failed => Ok(invalid_credentials()),
97 AuthStep::Challenge {
98 payload,
99 } => Ok(self.issue_challenge(identifier, method, payload)),
100 }
101 }
102
103 #[inline]
104 fn finalize_authentication(&self, identity: IdentityId) -> Result<AuthResponse, Error> {
105 let token = generate_session_token(&self.rng);
106 self.persist_token(&token, identity)?;
107 Ok(AuthResponse::Authenticated {
108 identity,
109 token,
110 })
111 }
112
113 #[inline]
114 fn issue_challenge(&self, identifier: &str, method: &str, payload: HashMap<String, String>) -> AuthResponse {
115 let challenge_id = self.challenges.create(
116 identifier.to_string(),
117 method.to_string(),
118 payload.clone(),
119 &self.clock,
120 &self.rng,
121 );
122 AuthResponse::Challenge {
123 challenge_id,
124 payload,
125 }
126 }
127
128 #[inline]
129 fn provider_for(&self, method: &str) -> Result<&dyn AuthenticationProvider, Error> {
130 self.auth_registry.get(method).ok_or_else(|| {
131 Error::from(AuthError::UnknownMethod {
132 method: method.to_string(),
133 })
134 })
135 }
136
137 fn authenticate_token(&self, credentials: HashMap<String, String>) -> Result<AuthResponse, Error> {
138 let token_value = match credentials.get("token") {
139 Some(t) if !t.is_empty() => t,
140 _ => return Ok(invalid_credentials()),
141 };
142
143 match self.validate_token(token_value) {
144 Some(token) => self.finalize_authentication(token.identity),
145 None => Ok(invalid_credentials()),
146 }
147 }
148
149 fn authenticate_challenge_response(
151 &self,
152 challenge_id: &str,
153 mut credentials: HashMap<String, String>,
154 ) -> Result<AuthResponse, Error> {
155 let Some(challenge) = self.challenges.consume(challenge_id) else {
156 return Ok(AuthResponse::Failed {
157 reason: "invalid or expired challenge".to_string(),
158 });
159 };
160
161 merge_challenge_payload(&mut credentials, &challenge.payload);
162
163 let mut txn = self.engine.begin_query()?;
164 let catalog = self.engine.catalog();
165
166 let ident = match catalog
167 .find_identity_by_name(&mut Transaction::Query(&mut txn), &challenge.identifier)?
168 {
169 Some(u) if u.enabled => u,
170 _ => return Ok(invalid_credentials()),
171 };
172
173 let Some(stored_auth) = catalog.find_authentication_by_identity_and_method(
174 &mut Transaction::Query(&mut txn),
175 ident.id,
176 &challenge.method,
177 )?
178 else {
179 return Ok(invalid_credentials());
180 };
181
182 let provider = self.provider_for(&challenge.method)?;
183 let step = provider.authenticate(&stored_auth.properties, &credentials)?;
184 respond_to_challenge_step(step, ident.id, self)
185 }
186}
187
188#[inline]
189fn merge_challenge_payload(credentials: &mut HashMap<String, String>, payload: &HashMap<String, String>) {
190 for (k, v) in payload {
191 credentials.entry(k.clone()).or_insert_with(|| v.clone());
192 }
193 credentials.remove("challenge_id");
194}
195
196#[inline]
197fn respond_to_challenge_step(
198 step: AuthStep,
199 identity: IdentityId,
200 service: &AuthService,
201) -> Result<AuthResponse, Error> {
202 match step {
203 AuthStep::Authenticated => service.finalize_authentication(identity),
204 AuthStep::Failed => Ok(invalid_credentials()),
205 AuthStep::Challenge {
206 ..
207 } => Ok(AuthResponse::Failed {
208 reason: "nested challenges are not supported".to_string(),
209 }),
210 }
211}
212
213#[inline]
214fn invalid_credentials() -> AuthResponse {
215 AuthResponse::Failed {
216 reason: "invalid credentials".to_string(),
217 }
218}