surrealdb_core/iam/
signup.rs

1use super::access::{authenticate_record, create_refresh_token_record};
2use crate::cnf::{INSECURE_FORWARD_ACCESS_ERRORS, SERVER_NAME};
3use crate::dbs::capabilities::ExperimentalTarget;
4use crate::dbs::Session;
5use crate::err::Error;
6use crate::iam::issue::{config, expiration};
7use crate::iam::token::Claims;
8use crate::iam::Auth;
9use crate::iam::{Actor, Level};
10use crate::kvs::{Datastore, LockType::*, TransactionType::*};
11use crate::sql::AccessType;
12use crate::sql::Object;
13use crate::sql::Value;
14use chrono::Utc;
15use jsonwebtoken::{encode, Header};
16use revision::revisioned;
17use serde::{Deserialize, Serialize};
18use std::sync::Arc;
19use uuid::Uuid;
20
21#[revisioned(revision = 1)]
22#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
23#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
24#[non_exhaustive]
25pub struct SignupData {
26	pub token: Option<String>,
27	pub refresh: Option<String>,
28}
29
30impl From<SignupData> for Value {
31	fn from(v: SignupData) -> Value {
32		let mut out = Object::default();
33		if let Some(token) = v.token {
34			out.insert("token".to_string(), token.into());
35		}
36		if let Some(refresh) = v.refresh {
37			out.insert("refresh".to_string(), refresh.into());
38		}
39		out.into()
40	}
41}
42
43pub async fn signup(
44	kvs: &Datastore,
45	session: &mut Session,
46	vars: Object,
47) -> Result<SignupData, Error> {
48	// Check vars contains only computed values
49	vars.validate_computed()?;
50	// Parse the specified variables
51	let ns = vars.get("NS").or_else(|| vars.get("ns"));
52	let db = vars.get("DB").or_else(|| vars.get("db"));
53	let ac = vars.get("AC").or_else(|| vars.get("ac"));
54	// Check if the parameters exist
55	match (ns, db, ac) {
56		(Some(ns), Some(db), Some(ac)) => {
57			// Process the provided values
58			let ns = ns.to_raw_string();
59			let db = db.to_raw_string();
60			let ac = ac.to_raw_string();
61			// Attempt to signup using specified access method
62			// Currently, signup is only supported at the database level
63			super::signup::db_access(kvs, session, ns, db, ac, vars).await
64		}
65		_ => Err(Error::InvalidSignup),
66	}
67}
68
69pub async fn db_access(
70	kvs: &Datastore,
71	session: &mut Session,
72	ns: String,
73	db: String,
74	ac: String,
75	vars: Object,
76) -> Result<SignupData, Error> {
77	// Create a new readonly transaction
78	let tx = kvs.transaction(Read, Optimistic).await?;
79	// Fetch the specified access method from storage
80	let access = tx.get_db_access(&ns, &db, &ac).await;
81	// Ensure that the transaction is cancelled
82	tx.cancel().await?;
83	// Check the provided access method exists
84	match access {
85		Ok(av) => {
86			// Check the access method type
87			// Currently, only the record access method supports signup
88			match &av.kind {
89				AccessType::Record(at) => {
90					// Check if the record access method supports issuing tokens
91					let iss = match &at.jwt.issue {
92						Some(iss) => iss,
93						_ => return Err(Error::AccessMethodMismatch),
94					};
95					match &at.signup {
96						// This record access allows signup
97						Some(val) => {
98							// Setup the query params
99							let vars = Some(vars.0);
100							// Setup the system session for finding the signup record
101							let mut sess = Session::editor().with_ns(&ns).with_db(&db);
102							sess.ip.clone_from(&session.ip);
103							sess.or.clone_from(&session.or);
104							// Compute the value with the params
105							match kvs.evaluate(val, &sess, vars).await {
106								// The signup value succeeded
107								Ok(val) => {
108									match val.record() {
109										// There is a record returned
110										Some(mut rid) => {
111											// Create the authentication key
112											let key = config(iss.alg, &iss.key)?;
113											// Create the authentication claim
114											let claims = Claims {
115												iss: Some(SERVER_NAME.to_owned()),
116												iat: Some(Utc::now().timestamp()),
117												nbf: Some(Utc::now().timestamp()),
118												exp: expiration(av.duration.token)?,
119												jti: Some(Uuid::new_v4().to_string()),
120												ns: Some(ns.to_owned()),
121												db: Some(db.to_owned()),
122												ac: Some(ac.to_owned()),
123												id: Some(rid.to_raw()),
124												..Claims::default()
125											};
126											// AUTHENTICATE clause
127											if let Some(au) = &av.authenticate {
128												// Setup the system session for finding the signin record
129												let mut sess =
130													Session::editor().with_ns(&ns).with_db(&db);
131												sess.rd = Some(rid.clone().into());
132												sess.tk = Some((&claims).into());
133												sess.ip.clone_from(&session.ip);
134												sess.or.clone_from(&session.or);
135												rid = authenticate_record(kvs, &sess, au).await?;
136											}
137											// Create refresh token if defined for the record access method
138											let refresh = match &at.bearer {
139												Some(_) => {
140													// TODO(gguillemas): Remove this once bearer access is no longer experimental
141													if !kvs.get_capabilities().allows_experimental(
142														&ExperimentalTarget::BearerAccess,
143													) {
144														debug!("Will not create refresh token with disabled bearer access feature");
145														None
146													} else {
147														Some(
148															create_refresh_token_record(
149																kvs,
150																av.name.clone(),
151																&ns,
152																&db,
153																rid.clone(),
154															)
155															.await?,
156														)
157													}
158												}
159												None => None,
160											};
161											// Log the authenticated access method info
162											trace!("Signing up with access method `{}`", ac);
163											// Create the authentication token
164											let enc =
165												encode(&Header::new(iss.alg.into()), &claims, &key);
166											// Set the authentication on the session
167											session.tk = Some((&claims).into());
168											session.ns = Some(ns.to_owned());
169											session.db = Some(db.to_owned());
170											session.ac = Some(ac.to_owned());
171											session.rd = Some(Value::from(rid.to_owned()));
172											session.exp = expiration(av.duration.session)?;
173											session.au = Arc::new(Auth::new(Actor::new(
174												rid.to_string(),
175												Default::default(),
176												Level::Record(ns, db, rid.to_string()),
177											)));
178											// Check the authentication token
179											match enc {
180												// The auth token was created successfully
181												Ok(token) => Ok(SignupData {
182													token: Some(token),
183													refresh,
184												}),
185												_ => Err(Error::TokenMakingFailed),
186											}
187										}
188										_ => Err(Error::NoRecordFound),
189									}
190								}
191								Err(e) => match e {
192									// If the SIGNUP clause throws a specific error, authentication fails with that error
193									Error::Thrown(_) => Err(e),
194									// If the SIGNUP clause failed due to an unexpected error, be more specific
195									// This allows clients to handle these errors, which may be retryable
196									Error::Tx(_) | Error::TxFailure | Error::TxRetryable => {
197										debug!("Unexpected error found while executing a SIGNUP clause: {e}");
198										Err(Error::UnexpectedAuth)
199									}
200									// Otherwise, return a generic error unless it should be forwarded
201									e => {
202										debug!("Record user signup query failed: {e}");
203										if *INSECURE_FORWARD_ACCESS_ERRORS {
204											Err(e)
205										} else {
206											Err(Error::AccessRecordSignupQueryFailed)
207										}
208									}
209								},
210							}
211						}
212						_ => Err(Error::AccessRecordNoSignup),
213					}
214				}
215				_ => Err(Error::AccessMethodMismatch),
216			}
217		}
218		_ => Err(Error::AccessNotFound),
219	}
220}
221
222#[cfg(test)]
223mod tests {
224	use super::*;
225	use crate::{dbs::Capabilities, iam::Role};
226	use chrono::Duration;
227	use std::collections::HashMap;
228
229	#[tokio::test]
230	async fn test_record_signup() {
231		// Test with valid parameters
232		{
233			let ds = Datastore::new("memory").await.unwrap();
234			let sess = Session::owner().with_ns("test").with_db("test");
235			ds.execute(
236				r#"
237				DEFINE ACCESS user ON DATABASE TYPE RECORD
238					SIGNIN (
239						SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
240					)
241					SIGNUP (
242						CREATE user CONTENT {
243							name: $user,
244							pass: crypto::argon2::generate($pass)
245						}
246					)
247					DURATION FOR SESSION 2h
248				;
249				"#,
250				&sess,
251				None,
252			)
253			.await
254			.unwrap();
255
256			// Signin with the user
257			let mut sess = Session {
258				ns: Some("test".to_string()),
259				db: Some("test".to_string()),
260				..Default::default()
261			};
262			let mut vars: HashMap<&str, Value> = HashMap::new();
263			vars.insert("user", "user".into());
264			vars.insert("pass", "pass".into());
265			let res = db_access(
266				&ds,
267				&mut sess,
268				"test".to_string(),
269				"test".to_string(),
270				"user".to_string(),
271				vars.into(),
272			)
273			.await;
274
275			assert!(res.is_ok(), "Failed to signup: {:?}", res);
276			assert_eq!(sess.ns, Some("test".to_string()));
277			assert_eq!(sess.db, Some("test".to_string()));
278			assert!(sess.au.id().starts_with("user:"));
279			assert!(sess.au.is_record());
280			assert_eq!(sess.au.level().ns(), Some("test"));
281			assert_eq!(sess.au.level().db(), Some("test"));
282			// Record users should not have roles.
283			assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
284			assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
285			assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
286			// Session expiration should match the defined duration
287			let exp = sess.exp.unwrap();
288			// Expiration should match the current time plus session duration with some margin
289			let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
290			let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
291			assert!(
292				exp > min_exp && exp < max_exp,
293				"Session expiration is expected to match the defined duration"
294			);
295		}
296
297		// Test with invalid parameters
298		{
299			let ds = Datastore::new("memory").await.unwrap();
300			let sess = Session::owner().with_ns("test").with_db("test");
301			ds.execute(
302				r#"
303				DEFINE ACCESS user ON DATABASE TYPE RECORD
304					SIGNIN (
305						SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
306					)
307					SIGNUP (
308						CREATE user CONTENT {
309							name: $user,
310							pass: crypto::argon2::generate($pass)
311						}
312					)
313					DURATION FOR SESSION 2h
314				;
315				"#,
316				&sess,
317				None,
318			)
319			.await
320			.unwrap();
321
322			// Signin with the user
323			let mut sess = Session {
324				ns: Some("test".to_string()),
325				db: Some("test".to_string()),
326				..Default::default()
327			};
328			let mut vars: HashMap<&str, Value> = HashMap::new();
329			// Password is missing
330			vars.insert("user", "user".into());
331			let res = db_access(
332				&ds,
333				&mut sess,
334				"test".to_string(),
335				"test".to_string(),
336				"user".to_string(),
337				vars.into(),
338			)
339			.await;
340
341			assert!(res.is_err(), "Unexpected successful signup: {:?}", res);
342		}
343	}
344
345	#[tokio::test]
346	async fn test_signup_record_with_refresh() {
347		use crate::iam::signin;
348
349		// Test without refresh
350		{
351			let ds = Datastore::new("memory").await.unwrap();
352			let sess = Session::owner().with_ns("test").with_db("test");
353			ds.execute(
354				r#"
355				DEFINE ACCESS user ON DATABASE TYPE RECORD
356					SIGNUP (
357						CREATE user CONTENT {
358							name: $user,
359							pass: crypto::argon2::generate($pass)
360						}
361					)
362					DURATION FOR GRANT 1w, FOR SESSION 2h
363				;
364
365				CREATE user:test CONTENT {
366					name: 'user',
367					pass: crypto::argon2::generate('pass')
368				}
369				"#,
370				&sess,
371				None,
372			)
373			.await
374			.unwrap();
375
376			// Signup with the user
377			let mut sess = Session {
378				ns: Some("test".to_string()),
379				db: Some("test".to_string()),
380				..Default::default()
381			};
382			let mut vars: HashMap<&str, Value> = HashMap::new();
383			vars.insert("user", "user".into());
384			vars.insert("pass", "pass".into());
385			let res = db_access(
386				&ds,
387				&mut sess,
388				"test".to_string(),
389				"test".to_string(),
390				"user".to_string(),
391				vars.into(),
392			)
393			.await;
394
395			match res {
396				Ok(data) => {
397					assert!(data.refresh.is_none(), "Refresh token was unexpectedly returned")
398				}
399				Err(e) => panic!("Failed to signup with credentials: {e}"),
400			}
401		}
402		// Test with refresh
403		{
404			let ds = Datastore::new("memory").await.unwrap().with_capabilities(
405				Capabilities::default().with_experimental(ExperimentalTarget::BearerAccess.into()),
406			);
407			let sess = Session::owner().with_ns("test").with_db("test");
408			ds.execute(
409				r#"
410				DEFINE ACCESS user ON DATABASE TYPE RECORD
411					SIGNUP (
412						CREATE user CONTENT {
413							name: $user,
414							pass: crypto::argon2::generate($pass)
415						}
416					)
417					WITH REFRESH
418					DURATION FOR GRANT 1w, FOR SESSION 2h
419				;
420
421				CREATE user:test CONTENT {
422					name: 'user',
423					pass: crypto::argon2::generate('pass')
424				}
425				"#,
426				&sess,
427				None,
428			)
429			.await
430			.unwrap();
431
432			// Signup with the user
433			let mut sess = Session {
434				ns: Some("test".to_string()),
435				db: Some("test".to_string()),
436				..Default::default()
437			};
438			let mut vars: HashMap<&str, Value> = HashMap::new();
439			vars.insert("user", "user".into());
440			vars.insert("pass", "pass".into());
441			let res = db_access(
442				&ds,
443				&mut sess,
444				"test".to_string(),
445				"test".to_string(),
446				"user".to_string(),
447				vars.into(),
448			)
449			.await;
450
451			assert!(res.is_ok(), "Failed to signup with credentials: {:?}", res);
452			let refresh = match res {
453				Ok(data) => match data.refresh {
454					Some(refresh) => refresh,
455					None => panic!("Refresh token was not returned"),
456				},
457				Err(e) => panic!("Failed to signup with credentials: {e}"),
458			};
459			assert_eq!(sess.ns, Some("test".to_string()));
460			assert_eq!(sess.db, Some("test".to_string()));
461			assert!(sess.au.id().starts_with("user:"));
462			assert!(sess.au.is_record());
463			assert_eq!(sess.au.level().ns(), Some("test"));
464			assert_eq!(sess.au.level().db(), Some("test"));
465			// Record users should not have roles
466			assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
467			assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
468			assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
469			// Expiration should match the defined duration
470			let exp = sess.exp.unwrap();
471			// Expiration should match the current time plus session duration with some margin
472			let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
473			let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
474			assert!(
475				exp > min_exp && exp < max_exp,
476				"Session expiration is expected to follow the defined duration"
477			);
478			// Signin with the refresh token
479			let mut vars: HashMap<&str, Value> = HashMap::new();
480			vars.insert("refresh", refresh.clone().into());
481			let res = signin::db_access(
482				&ds,
483				&mut sess,
484				"test".to_string(),
485				"test".to_string(),
486				"user".to_string(),
487				vars.into(),
488			)
489			.await;
490			// Authentication should be identical as with user credentials
491			match res {
492				Ok(data) => match data.refresh {
493					Some(new_refresh) => assert!(
494						new_refresh != refresh,
495						"New refresh token is identical to used one"
496					),
497					None => panic!("Refresh token was not returned"),
498				},
499				Err(e) => panic!("Failed to signin with credentials: {e}"),
500			};
501			assert_eq!(sess.ns, Some("test".to_string()));
502			assert_eq!(sess.db, Some("test".to_string()));
503			assert!(sess.au.id().starts_with("user:"));
504			assert!(sess.au.is_record());
505			assert_eq!(sess.au.level().ns(), Some("test"));
506			assert_eq!(sess.au.level().db(), Some("test"));
507			// Record users should not have roles
508			assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
509			assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
510			assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
511			// Expiration should match the defined duration
512			let exp = sess.exp.unwrap();
513			// Expiration should match the current time plus session duration with some margin
514			let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
515			let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
516			assert!(
517				exp > min_exp && exp < max_exp,
518				"Session expiration is expected to follow the defined duration"
519			);
520			// Attempt to sign in with the original refresh token
521			let mut vars: HashMap<&str, Value> = HashMap::new();
522			vars.insert("refresh", refresh.into());
523			let res = signin::db_access(
524				&ds,
525				&mut sess,
526				"test".to_string(),
527				"test".to_string(),
528				"user".to_string(),
529				vars.into(),
530			)
531			.await;
532			match res {
533				Ok(data) => panic!("Unexpected successful signin: {:?}", data),
534				Err(Error::InvalidAuth) => {} // ok
535				Err(e) => panic!("Expected InvalidAuth, but got: {e}"),
536			}
537		}
538	}
539
540	#[tokio::test]
541	async fn test_record_signup_with_jwt_issuer() {
542		use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
543		// Test with valid parameters
544		{
545			let public_key = r#"-----BEGIN PUBLIC KEY-----
546MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
5474lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
548+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
549kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
5500iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
551cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
552mwIDAQAB
553-----END PUBLIC KEY-----"#;
554			let private_key = r#"-----BEGIN PRIVATE KEY-----
555MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
556MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
557NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
558qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
559p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
560ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
561VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
562laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
563sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
564mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
565dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
566ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
567DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
568N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
5690l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
570t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
571AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
57248RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
573DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
574xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
575mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
5762bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
577et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
578VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
579TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
580dn/RsYEONbwQSjIfMPkvxF+8HQ==
581-----END PRIVATE KEY-----"#;
582			let ds = Datastore::new("memory").await.unwrap();
583			let sess = Session::owner().with_ns("test").with_db("test");
584			ds.execute(
585				&format!(
586					r#"
587				DEFINE ACCESS user ON DATABASE TYPE RECORD
588					SIGNIN (
589						SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
590					)
591					SIGNUP (
592						CREATE user CONTENT {{
593							name: $user,
594							pass: crypto::argon2::generate($pass)
595						}}
596					)
597				    WITH JWT ALGORITHM RS256 KEY '{public_key}'
598				        WITH ISSUER KEY '{private_key}'
599					DURATION FOR SESSION 2h, FOR TOKEN 15m
600				;
601
602				CREATE user:test CONTENT {{
603					name: 'user',
604					pass: crypto::argon2::generate('pass')
605				}}
606				"#
607				),
608				&sess,
609				None,
610			)
611			.await
612			.unwrap();
613
614			// Signin with the user
615			let mut sess = Session {
616				ns: Some("test".to_string()),
617				db: Some("test".to_string()),
618				..Default::default()
619			};
620			let mut vars: HashMap<&str, Value> = HashMap::new();
621			vars.insert("user", "user".into());
622			vars.insert("pass", "pass".into());
623			let res = db_access(
624				&ds,
625				&mut sess,
626				"test".to_string(),
627				"test".to_string(),
628				"user".to_string(),
629				vars.into(),
630			)
631			.await;
632
633			assert!(res.is_ok(), "Failed to signup: {:?}", res);
634			assert_eq!(sess.ns, Some("test".to_string()));
635			assert_eq!(sess.db, Some("test".to_string()));
636			assert!(sess.au.id().starts_with("user:"));
637			assert!(sess.au.is_record());
638			assert_eq!(sess.au.level().ns(), Some("test"));
639			assert_eq!(sess.au.level().db(), Some("test"));
640			// Record users should not have roles.
641			assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
642			assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
643			assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
644			// Session expiration should always be set for tokens issued by SurrealDB
645			let exp = sess.exp.unwrap();
646			// Expiration should match the current time plus session duration with some margin
647			let min_sess_exp =
648				(Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
649			let max_sess_exp =
650				(Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
651			assert!(
652				exp > min_sess_exp && exp < max_sess_exp,
653				"Session expiration is expected to follow access method duration"
654			);
655
656			// Decode token and check that it has been issued as intended
657			if let Ok(SignupData {
658				token: Some(tk),
659				..
660			}) = res
661			{
662				// Check that token can be verified with the defined algorithm
663				let val = Validation::new(Algorithm::RS256);
664				// Check that token can be verified with the defined public key
665				let token_data = decode::<Claims>(
666					&tk,
667					&DecodingKey::from_rsa_pem(public_key.as_ref()).unwrap(),
668					&val,
669				)
670				.unwrap();
671				// Check that token has been issued with the defined algorithm
672				assert_eq!(token_data.header.alg, Algorithm::RS256);
673				// Check that token expiration matches the defined duration
674				// Expiration should match the current time plus token duration with some margin
675				let exp = match token_data.claims.exp {
676					Some(exp) => exp,
677					_ => panic!("Token is missing expiration claim"),
678				};
679				let min_tk_exp =
680					(Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
681				let max_tk_exp =
682					(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
683				assert!(
684					exp > min_tk_exp && exp < max_tk_exp,
685					"Token expiration is expected to follow issuer duration"
686				);
687				// Check required token claims
688				assert_eq!(token_data.claims.ns, Some("test".to_string()));
689				assert_eq!(token_data.claims.db, Some("test".to_string()));
690				assert!(token_data.claims.id.unwrap().starts_with("user:"));
691				assert_eq!(token_data.claims.ac, Some("user".to_string()));
692			} else {
693				panic!("Token could not be extracted from result")
694			}
695		}
696	}
697
698	#[tokio::test]
699	async fn test_signup_record_and_authenticate_clause() {
700		// Test with correct credentials
701		{
702			let ds = Datastore::new("memory").await.unwrap();
703			let sess = Session::owner().with_ns("test").with_db("test");
704			ds.execute(
705				r#"
706				DEFINE ACCESS user ON DATABASE TYPE RECORD
707					SIGNUP (
708						CREATE type::thing('user', $id)
709					)
710					AUTHENTICATE (
711						-- Simple example increasing the record identifier by one
712					    SELECT * FROM type::thing('user', record::id($auth) + 1)
713					)
714					DURATION FOR SESSION 2h
715				;
716
717				CREATE user:2;
718				"#,
719				&sess,
720				None,
721			)
722			.await
723			.unwrap();
724
725			// Signin with the user
726			let mut sess = Session {
727				ns: Some("test".to_string()),
728				db: Some("test".to_string()),
729				..Default::default()
730			};
731			let mut vars: HashMap<&str, Value> = HashMap::new();
732			vars.insert("id", 1.into());
733			let res = db_access(
734				&ds,
735				&mut sess,
736				"test".to_string(),
737				"test".to_string(),
738				"user".to_string(),
739				vars.into(),
740			)
741			.await;
742
743			assert!(res.is_ok(), "Failed to signup with credentials: {:?}", res);
744			assert_eq!(sess.ns, Some("test".to_string()));
745			assert_eq!(sess.db, Some("test".to_string()));
746			assert_eq!(sess.ac, Some("user".to_string()));
747			assert_eq!(sess.au.id(), "user:2");
748			assert!(sess.au.is_record());
749			assert_eq!(sess.au.level().ns(), Some("test"));
750			assert_eq!(sess.au.level().db(), Some("test"));
751			assert_eq!(sess.au.level().id(), Some("user:2"));
752			// Record users should not have roles
753			assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
754			assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
755			assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
756			// Expiration should match the defined duration
757			let exp = sess.exp.unwrap();
758			// Expiration should match the current time plus session duration with some margin
759			let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
760			let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
761			assert!(
762				exp > min_exp && exp < max_exp,
763				"Session expiration is expected to follow the defined duration"
764			);
765		}
766
767		// Test with correct credentials and "realistic" scenario
768		{
769			let ds = Datastore::new("memory").await.unwrap();
770			let sess = Session::owner().with_ns("test").with_db("test");
771			ds.execute(
772				r#"
773				DEFINE ACCESS owner ON DATABASE TYPE RECORD
774					SIGNUP (
775						-- Allow anyone to sign up as a new company
776						-- This automatically creates an owner with the same credentials
777						CREATE company CONTENT {
778							email: $email,
779							pass: crypto::argon2::generate($pass),
780							owner: (CREATE employee CONTENT {
781								email: $email,
782								pass: $pass,
783							}),
784						}
785					)
786					SIGNIN (
787						-- Allow company owners to log in directly with the company account
788						SELECT * FROM company WHERE email = $email AND crypto::argon2::compare(pass, $pass)
789					)
790					AUTHENTICATE (
791						-- If logging in with a company account, the session will be authenticated as the first owner
792						IF record::tb($auth) = "company" {
793							RETURN SELECT VALUE owner FROM company WHERE id = $auth
794						}
795					)
796					DURATION FOR SESSION 2h
797				;
798
799				CREATE company:1 CONTENT {
800					email: "info@example.com",
801					pass: crypto::argon2::generate("company-password"),
802					owner: employee:2,
803				};
804				CREATE employee:1 CONTENT {
805					email: "member@example.com",
806					pass: crypto::argon2::generate("member-password"),
807				};
808				CREATE employee:2 CONTENT {
809					email: "owner@example.com",
810					pass: crypto::argon2::generate("owner-password"),
811				};
812				"#,
813				&sess,
814				None,
815			)
816			.await
817			.unwrap();
818
819			// Signin with the user
820			let mut sess = Session {
821				ns: Some("test".to_string()),
822				db: Some("test".to_string()),
823				..Default::default()
824			};
825			let mut vars: HashMap<&str, Value> = HashMap::new();
826			vars.insert("email", "info@example.com".into());
827			vars.insert("pass", "company-password".into());
828			let res = db_access(
829				&ds,
830				&mut sess,
831				"test".to_string(),
832				"test".to_string(),
833				"owner".to_string(),
834				vars.into(),
835			)
836			.await;
837
838			assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
839			assert_eq!(sess.ns, Some("test".to_string()));
840			assert_eq!(sess.db, Some("test".to_string()));
841			assert_eq!(sess.ac, Some("owner".to_string()));
842			assert!(sess.au.id().starts_with("employee:"));
843			assert!(sess.au.is_record());
844			assert_eq!(sess.au.level().ns(), Some("test"));
845			assert_eq!(sess.au.level().db(), Some("test"));
846			assert!(sess.au.level().id().unwrap().starts_with("employee:"));
847			// Record users should not have roles
848			assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
849			assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
850			assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
851			// Expiration should match the defined duration
852			let exp = sess.exp.unwrap();
853			// Expiration should match the current time plus session duration with some margin
854			let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
855			let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
856			assert!(
857				exp > min_exp && exp < max_exp,
858				"Session expiration is expected to follow the defined duration"
859			);
860		}
861
862		// Test being able to fail authentication
863		{
864			let ds = Datastore::new("memory").await.unwrap();
865			let sess = Session::owner().with_ns("test").with_db("test");
866			ds.execute(
867				r#"
868				DEFINE ACCESS user ON DATABASE TYPE RECORD
869					SIGNUP (
870					   CREATE type::thing('user', $id)
871					)
872					AUTHENTICATE {
873					    -- Not just signin, this clause runs across signin, signup and authenticate, which makes it a nice place to centralize logic
874					    IF !$auth.enabled {
875							THROW "This user is not enabled";
876						};
877
878						-- Always need to return the user id back, otherwise auth generically fails
879						RETURN $auth;
880					}
881					DURATION FOR SESSION 2h
882				;
883				"#,
884				&sess,
885				None,
886			)
887			.await
888			.unwrap();
889
890			// Signin with the user
891			let mut sess = Session {
892				ns: Some("test".to_string()),
893				db: Some("test".to_string()),
894				..Default::default()
895			};
896			let mut vars: HashMap<&str, Value> = HashMap::new();
897			vars.insert("id", 1.into());
898			let res = db_access(
899				&ds,
900				&mut sess,
901				"test".to_string(),
902				"test".to_string(),
903				"user".to_string(),
904				vars.into(),
905			)
906			.await;
907
908			match res {
909				Err(Error::Thrown(e)) if e == "This user is not enabled" => {} // ok
910				res => panic!(
911				    "Expected authentication to failed due to user not being enabled, but instead received: {:?}",
912					res
913				),
914			}
915		}
916
917		// Test AUTHENTICATE clause not returning a value
918		{
919			let ds = Datastore::new("memory").await.unwrap();
920			let sess = Session::owner().with_ns("test").with_db("test");
921			ds.execute(
922				r#"
923				DEFINE ACCESS user ON DATABASE TYPE RECORD
924					SIGNUP (
925					   CREATE type::thing('user', $id)
926					)
927					AUTHENTICATE {}
928					DURATION FOR SESSION 2h
929				;
930				"#,
931				&sess,
932				None,
933			)
934			.await
935			.unwrap();
936
937			// Signin with the user
938			let mut sess = Session {
939				ns: Some("test".to_string()),
940				db: Some("test".to_string()),
941				..Default::default()
942			};
943			let mut vars: HashMap<&str, Value> = HashMap::new();
944			vars.insert("id", 1.into());
945			let res = db_access(
946				&ds,
947				&mut sess,
948				"test".to_string(),
949				"test".to_string(),
950				"user".to_string(),
951				vars.into(),
952			)
953			.await;
954
955			match res {
956				Err(Error::InvalidAuth) => {} // ok
957				res => panic!(
958					"Expected authentication to generally fail, but instead received: {:?}",
959					res
960				),
961			}
962		}
963	}
964
965	#[tokio::test]
966	#[ignore = "flaky"]
967	async fn test_signup_record_transaction_conflict() {
968		// Test SIGNUP failing due to datastore transaction conflict
969		{
970			let ds = Datastore::new("memory").await.unwrap();
971			let sess = Session::owner().with_ns("test").with_db("test");
972			ds.execute(
973				r#"
974				DEFINE ACCESS user ON DATABASE TYPE RECORD
975					SIGNIN (
976						SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
977					)
978					SIGNUP {
979						-- Concurrently write to the same document
980						UPSERT count:1 SET count += 1;
981						-- Increase the duration of the transaction
982						sleep(500ms);
983						-- Continue with authentication
984						RETURN (CREATE user CONTENT {
985							name: $user,
986							pass: crypto::argon2::generate($pass)
987						})
988					}
989					DURATION FOR SESSION 2h
990				;
991				"#,
992				&sess,
993				None,
994			)
995			.await
996			.unwrap();
997
998			// Sign up with the user twice at the same time
999			let mut sess1 = Session {
1000				ns: Some("test".to_string()),
1001				db: Some("test".to_string()),
1002				..Default::default()
1003			};
1004			let mut sess2 = Session {
1005				ns: Some("test".to_string()),
1006				db: Some("test".to_string()),
1007				..Default::default()
1008			};
1009			let mut vars: HashMap<&str, Value> = HashMap::new();
1010			vars.insert("user", "user".into());
1011			vars.insert("pass", "pass".into());
1012
1013			let (res1, res2) = tokio::join!(
1014				db_access(
1015					&ds,
1016					&mut sess1,
1017					"test".to_string(),
1018					"test".to_string(),
1019					"user".to_string(),
1020					vars.clone().into(),
1021				),
1022				db_access(
1023					&ds,
1024					&mut sess2,
1025					"test".to_string(),
1026					"test".to_string(),
1027					"user".to_string(),
1028					vars.into(),
1029				)
1030			);
1031
1032			match (res1, res2) {
1033				(Ok(r1), Ok(r2)) => panic!("Expected authentication to fail in one instance, but instead received: {:?} and {:?}", r1, r2),
1034				(Err(e1), Err(e2)) => panic!("Expected authentication to fail in one instance, but instead received: {:?} and {:?}", e1, e2),
1035				(Err(e1), Ok(_)) => match &e1 {
1036						Error::UnexpectedAuth => {} // ok
1037						e => panic!("Expected authentication to return an UnexpectedAuth error, but insted got: {e}")
1038				}
1039				(Ok(_), Err(e2)) => match &e2 {
1040						Error::UnexpectedAuth => {} // ok
1041						e => panic!("Expected authentication to return an UnexpectedAuth error, but insted got: {e}")
1042				}
1043			}
1044		}
1045
1046		// Test AUTHENTICATE failing due to datastore transaction conflict
1047		{
1048			let ds = Datastore::new("memory").await.unwrap();
1049			let sess = Session::owner().with_ns("test").with_db("test");
1050			ds.execute(
1051				r#"
1052				DEFINE ACCESS user ON DATABASE TYPE RECORD
1053					SIGNUP (
1054						CREATE type::thing('user', $id)
1055					)
1056					AUTHENTICATE {
1057						-- Concurrently write to the same document
1058						UPSERT count:1 SET count += 1;
1059						-- Increase the duration of the transaction
1060						sleep(500ms);
1061						-- Continue with authentication
1062						$auth.id
1063					}
1064					DURATION FOR SESSION 2h
1065				;
1066				"#,
1067				&sess,
1068				None,
1069			)
1070			.await
1071			.unwrap();
1072
1073			// Sign up with the user twice at the same time
1074			let mut sess1 = Session {
1075				ns: Some("test".to_string()),
1076				db: Some("test".to_string()),
1077				..Default::default()
1078			};
1079			let mut sess2 = Session {
1080				ns: Some("test".to_string()),
1081				db: Some("test".to_string()),
1082				..Default::default()
1083			};
1084			let mut vars: HashMap<&str, Value> = HashMap::new();
1085			vars.insert("id", 1.into());
1086
1087			let (res1, res2) = tokio::join!(
1088				db_access(
1089					&ds,
1090					&mut sess1,
1091					"test".to_string(),
1092					"test".to_string(),
1093					"user".to_string(),
1094					vars.clone().into(),
1095				),
1096				db_access(
1097					&ds,
1098					&mut sess2,
1099					"test".to_string(),
1100					"test".to_string(),
1101					"user".to_string(),
1102					vars.into(),
1103				)
1104			);
1105
1106			match (res1, res2) {
1107				(Ok(r1), Ok(r2)) => panic!("Expected authentication to fail in one instance, but instead received: {:?} and {:?}", r1, r2),
1108				(Err(e1), Err(e2)) => panic!("Expected authentication to fail in one instance, but instead received: {:?} and {:?}", e1, e2),
1109				(Err(e1), Ok(_)) => match &e1 {
1110						Error::UnexpectedAuth => {} // ok
1111						e => panic!("Expected authentication to return an UnexpectedAuth error, but insted got: {e}")
1112				}
1113				(Ok(_), Err(e2)) => match &e2 {
1114						Error::UnexpectedAuth => {} // ok
1115						e => panic!("Expected authentication to return an UnexpectedAuth error, but insted got: {e}")
1116				}
1117			}
1118		}
1119	}
1120}