diff --git a/core/src/iam/verify.rs b/core/src/iam/verify.rs index 7427c86e..5a98e315 100644 --- a/core/src/iam/verify.rs +++ b/core/src/iam/verify.rs @@ -2266,9 +2266,89 @@ mod tests { ALGORITHM HS512 KEY '{secret}' AUTHENTICATE {{ IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; }} - DURATION FOR SESSION 2h + DURATION FOR SESSION 2h + ; + "# + ) + .as_str(), + &sess, + None, + ) + .await + .unwrap(); + + // Prepare the claims object + let mut claims = claims.clone(); + claims.roles = None; + // Create the token + let enc = encode(&HEADER, &claims, &key).unwrap(); + // Signin with the token + let mut sess = Session::default(); + let res = token(&ds, &mut sess, &enc).await; + + assert!(res.is_ok(), "Failed to signin with token: {:?}", res); + assert_eq!(sess.ns, Some("test".to_string())); + assert_eq!(sess.db, Some("test".to_string())); + assert_eq!(sess.ac, Some("user".to_string())); + assert!(sess.au.is_db()); + assert_eq!(sess.au.level().ns(), Some("test")); + assert_eq!(sess.au.level().db(), Some("test")); + // Record users should not have roles + assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role"); + assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role"); + assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); + // Expiration should match the defined duration + let exp = sess.exp.unwrap(); + // Expiration should match the current time plus session duration with some margin + let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp(); + let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to follow the defined duration" + ); + } + + // Test with correct "iss" and "aud" claims, with multiple audiences + { + let secret = "jwt_secret"; + let key = EncodingKey::from_secret(secret.as_ref()); + let claims = Claims { + iss: Some("surrealdb-test".to_string()), + aud: Some(Audience::Multiple(vec![ + "invalid".to_string(), + "surrealdb-test".to_string(), + ])), + iat: Some(Utc::now().timestamp()), + nbf: Some(Utc::now().timestamp()), + exp: Some((Utc::now() + Duration::hours(1)).timestamp()), + ns: Some("test".to_string()), + db: Some("test".to_string()), + ac: Some("user".to_string()), + ..Claims::default() + }; + + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + ds.execute( + format!( + r#" + DEFINE ACCESS user ON DATABASE TYPE JWT + ALGORITHM HS512 KEY '{secret}' + AUTHENTICATE {{ + IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; + }} + DURATION FOR SESSION 2h ; "# ) @@ -2335,10 +2415,14 @@ mod tests { ALGORITHM HS512 KEY '{secret}' AUTHENTICATE {{ IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; }} DURATION FOR SESSION 2h - ; + ; "# ) .as_str(), @@ -2358,7 +2442,7 @@ mod tests { let res = token(&ds, &mut sess, &enc).await; match res { - Err(Error::Thrown(e)) if e == "Invalid token audience" => {} // ok + Err(Error::Thrown(e)) if e == "Invalid token audience string" => {} // ok res => panic!( "Expected authentication to failed due to invalid token audience, but instead received: {:?}", res @@ -2366,6 +2450,68 @@ mod tests { } } + // Test with correct "iss" claim but incorrect "aud" claim, with multiple audiences + { + let secret = "jwt_secret"; + let key = EncodingKey::from_secret(secret.as_ref()); + let claims = Claims { + iss: Some("surrealdb-test".to_string()), + aud: Some(Audience::Multiple(vec![ + "surrealdb-test-different".to_string(), + "invalid".to_string(), + ])), + iat: Some(Utc::now().timestamp()), + nbf: Some(Utc::now().timestamp()), + exp: Some((Utc::now() + Duration::hours(1)).timestamp()), + ns: Some("test".to_string()), + db: Some("test".to_string()), + ac: Some("user".to_string()), + ..Claims::default() + }; + + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + ds.execute( + format!( + r#" + DEFINE ACCESS user ON DATABASE TYPE JWT + ALGORITHM HS512 KEY '{secret}' + AUTHENTICATE {{ + IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; + }} + DURATION FOR SESSION 2h + "# + ) + .as_str(), + &sess, + None, + ) + .await + .unwrap(); + + // Prepare the claims object + let mut claims = claims.clone(); + claims.roles = None; + // Create the token + let enc = encode(&HEADER, &claims, &key).unwrap(); + // Signin with the token + let mut sess = Session::default(); + let res = token(&ds, &mut sess, &enc).await; + + match res { + Err(Error::Thrown(e)) if e == "Invalid token audience array" => {} // ok + res => panic!( + "Expected authentication to failed due to invalid token audience array, but instead received: {:?}", + res + ), + } + } + // Test with correct "iss" claim but incorrect "aud" claim // In this case, something is returned by the clause, which returns a generic error { @@ -2392,7 +2538,11 @@ mod tests { ALGORITHM HS512 KEY '{secret}' AUTHENTICATE {{ IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }}; - IF $token.aud != "surrealdb-test" {{ RETURN "FAIL" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ RETURN "FAIL" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ RETURN "FAIL" }} + }}; }} DURATION FOR SESSION 2h ; @@ -2450,9 +2600,87 @@ mod tests { ALGORITHM HS512 KEY '{secret}' AUTHENTICATE {{ IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; }} - DURATION FOR SESSION 2h + DURATION FOR SESSION 2h + ; + "# + ) + .as_str(), + &sess, + None, + ) + .await + .unwrap(); + + // Prepare the claims object + let mut claims = claims.clone(); + claims.roles = None; + // Create the token + let enc = encode(&HEADER, &claims, &key).unwrap(); + // Signin with the token + let mut sess = Session::default(); + let res = token(&ds, &mut sess, &enc).await; + + assert!(res.is_ok(), "Failed to signin with token: {:?}", res); + assert_eq!(sess.ns, Some("test".to_string())); + assert_eq!(sess.ac, Some("user".to_string())); + assert!(sess.au.is_ns()); + assert_eq!(sess.au.level().ns(), Some("test")); + assert_eq!(sess.au.level().db(), None); + // Record users should not have roles + assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role"); + assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role"); + assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); + // Expiration should match the defined duration + let exp = sess.exp.unwrap(); + // Expiration should match the current time plus session duration with some margin + let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp(); + let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to follow the defined duration" + ); + } + + // Test with correct "iss" and "aud" claims, with multiple audiences + { + let secret = "jwt_secret"; + let key = EncodingKey::from_secret(secret.as_ref()); + let claims = Claims { + iss: Some("surrealdb-test".to_string()), + aud: Some(Audience::Multiple(vec![ + "invalid".to_string(), + "surrealdb-test".to_string(), + ])), + iat: Some(Utc::now().timestamp()), + nbf: Some(Utc::now().timestamp()), + exp: Some((Utc::now() + Duration::hours(1)).timestamp()), + ns: Some("test".to_string()), + ac: Some("user".to_string()), + ..Claims::default() + }; + + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + ds.execute( + format!( + r#" + DEFINE ACCESS user ON NAMESPACE TYPE JWT + ALGORITHM HS512 KEY '{secret}' + AUTHENTICATE {{ + IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; + }} + DURATION FOR SESSION 2h ; "# ) @@ -2474,6 +2702,7 @@ mod tests { assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert_eq!(sess.ns, Some("test".to_string())); + assert_eq!(sess.db, None); assert_eq!(sess.ac, Some("user".to_string())); assert!(sess.au.is_ns()); assert_eq!(sess.au.level().ns(), Some("test")); @@ -2517,9 +2746,13 @@ mod tests { ALGORITHM HS512 KEY '{secret}' AUTHENTICATE {{ IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; }} - DURATION FOR SESSION 2h + DURATION FOR SESSION 2h ; "# ) @@ -2540,7 +2773,7 @@ mod tests { let res = token(&ds, &mut sess, &enc).await; match res { - Err(Error::Thrown(e)) if e == "Invalid token audience" => {} // ok + Err(Error::Thrown(e)) if e == "Invalid token audience string" => {} // ok res => panic!( "Expected authentication to failed due to invalid token audience, but instead received: {:?}", res @@ -2548,6 +2781,68 @@ mod tests { } } + // Test with correct "iss" claim but incorrect "aud" claim, with multiple audiences + { + let secret = "jwt_secret"; + let key = EncodingKey::from_secret(secret.as_ref()); + let claims = Claims { + iss: Some("surrealdb-test".to_string()), + aud: Some(Audience::Multiple(vec![ + "surrealdb-test-different".to_string(), + "invalid".to_string(), + ])), + iat: Some(Utc::now().timestamp()), + nbf: Some(Utc::now().timestamp()), + exp: Some((Utc::now() + Duration::hours(1)).timestamp()), + ns: Some("test".to_string()), + ac: Some("user".to_string()), + ..Claims::default() + }; + + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + ds.execute( + format!( + r#" + DEFINE ACCESS user ON NAMESPACE TYPE JWT + ALGORITHM HS512 KEY '{secret}' + AUTHENTICATE {{ + IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; + }} + DURATION FOR SESSION 2h + ; + "# + ) + .as_str(), + &sess, + None, + ) + .await + .unwrap(); + + // Prepare the claims object + let mut claims = claims.clone(); + claims.roles = None; + // Create the token + let enc = encode(&HEADER, &claims, &key).unwrap(); + // Signin with the token + let mut sess = Session::default(); + let res = token(&ds, &mut sess, &enc).await; + + match res { + Err(Error::Thrown(e)) if e == "Invalid token audience array" => {} // ok + res => panic!( + "Expected authentication to failed due to invalid token audience array, but instead received: {:?}", + res + ), + } + } + // Test with correct "iss" claim but incorrect "aud" claim // In this case, something is returned by the clause, which returns a generic error { @@ -2573,7 +2868,11 @@ mod tests { ALGORITHM HS512 KEY '{secret}' AUTHENTICATE {{ IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }}; - IF $token.aud != "surrealdb-test" {{ RETURN "FAIL" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ RETURN "FAIL" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ RETURN "FAIL" }} + }}; }} DURATION FOR SESSION 2h ; @@ -2630,7 +2929,11 @@ mod tests { ALGORITHM HS512 KEY '{secret}' AUTHENTICATE {{ IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; }} DURATION FOR SESSION 2h ; @@ -2672,6 +2975,80 @@ mod tests { ); } + // Test with correct "iss" and "aud" claims, with multiple audiences + { + let secret = "jwt_secret"; + let key = EncodingKey::from_secret(secret.as_ref()); + let claims = Claims { + iss: Some("surrealdb-test".to_string()), + aud: Some(Audience::Multiple(vec![ + "invalid".to_string(), + "surrealdb-test".to_string(), + ])), + iat: Some(Utc::now().timestamp()), + nbf: Some(Utc::now().timestamp()), + exp: Some((Utc::now() + Duration::hours(1)).timestamp()), + ac: Some("user".to_string()), + ..Claims::default() + }; + + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + ds.execute( + format!( + r#" + DEFINE ACCESS user ON ROOT TYPE JWT + ALGORITHM HS512 KEY '{secret}' + AUTHENTICATE {{ + IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; + }} + DURATION FOR SESSION 2h + ; + "# + ) + .as_str(), + &sess, + None, + ) + .await + .unwrap(); + + // Prepare the claims object + let mut claims = claims.clone(); + claims.roles = None; + // Create the token + let enc = encode(&HEADER, &claims, &key).unwrap(); + // Signin with the token + let mut sess = Session::default(); + let res = token(&ds, &mut sess, &enc).await; + + assert!(res.is_ok(), "Failed to signin with token: {:?}", res); + assert_eq!(sess.ns, None); + assert_eq!(sess.db, None); + assert_eq!(sess.ac, Some("user".to_string())); + assert!(sess.au.is_root()); + assert_eq!(sess.au.level().ns(), None); + assert_eq!(sess.au.level().db(), None); + // Record users should not have roles + assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role"); + assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role"); + assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); + // Expiration should match the defined duration + let exp = sess.exp.unwrap(); + // Expiration should match the current time plus session duration with some margin + let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp(); + let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to follow the defined duration" + ); + } + // Test with correct "iss" claim but incorrect "aud" claim { let secret = "jwt_secret"; @@ -2695,7 +3072,11 @@ mod tests { ALGORITHM HS512 KEY '{secret}' AUTHENTICATE {{ IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; }} DURATION FOR SESSION 2h ; @@ -2718,7 +3099,7 @@ mod tests { let res = token(&ds, &mut sess, &enc).await; match res { - Err(Error::Thrown(e)) if e == "Invalid token audience" => {} // ok + Err(Error::Thrown(e)) if e == "Invalid token audience string" => {} // ok res => panic!( "Expected authentication to failed due to invalid token audience, but instead received: {:?}", res @@ -2726,6 +3107,66 @@ mod tests { } } + // Test with correct "iss" claim but incorrect "aud" claim, with multiple audiences + { + let secret = "jwt_secret"; + let key = EncodingKey::from_secret(secret.as_ref()); + let claims = Claims { + iss: Some("surrealdb-test".to_string()), + aud: Some(Audience::Multiple(vec![ + "surrealdb-test-different".to_string(), + "invalid".to_string(), + ])), + iat: Some(Utc::now().timestamp()), + nbf: Some(Utc::now().timestamp()), + exp: Some((Utc::now() + Duration::hours(1)).timestamp()), + ac: Some("user".to_string()), + ..Claims::default() + }; + + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + ds.execute( + format!( + r#" + DEFINE ACCESS user ON ROOT TYPE JWT + ALGORITHM HS512 KEY '{secret}' + AUTHENTICATE {{ + IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} + }}; + }} + DURATION FOR SESSION 2h + "# + ) + .as_str(), + &sess, + None, + ) + .await + .unwrap(); + + // Prepare the claims object + let mut claims = claims.clone(); + claims.roles = None; + // Create the token + let enc = encode(&HEADER, &claims, &key).unwrap(); + // Signin with the token + let mut sess = Session::default(); + let res = token(&ds, &mut sess, &enc).await; + + match res { + Err(Error::Thrown(e)) if e == "Invalid token audience array" => {} // ok + res => panic!( + "Expected authentication to failed due to invalid token audience array, but instead received: {:?}", + res + ), + } + } + // Test with correct "iss" claim but incorrect "aud" claim // In this case, something is returned by the clause, which returns a generic error { @@ -2750,7 +3191,11 @@ mod tests { ALGORITHM HS512 KEY '{secret}' AUTHENTICATE {{ IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }}; - IF $token.aud != "surrealdb-test" {{ RETURN "FAIL" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ RETURN "FAIL" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ RETURN "FAIL" }} + }}; }} DURATION FOR SESSION 2h ;