Check for single and multiple token audiences (#4493)

This commit is contained in:
Gerard Guillemas Martos 2024-08-13 13:40:16 +02:00 committed by GitHub
parent 89f1de825a
commit a8b96936f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2266,7 +2266,87 @@ mod tests {
ALGORITHM HS512 KEY '{secret}' ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{ AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; 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(),
&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 DURATION FOR SESSION 2h
; ;
@ -2335,7 +2415,11 @@ mod tests {
ALGORITHM HS512 KEY '{secret}' ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{ AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; 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
; ;
@ -2358,7 +2442,7 @@ mod tests {
let res = token(&ds, &mut sess, &enc).await; let res = token(&ds, &mut sess, &enc).await;
match res { 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!( res => panic!(
"Expected authentication to failed due to invalid token audience, but instead received: {:?}", "Expected authentication to failed due to invalid token audience, but instead received: {:?}",
res 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 // Test with correct "iss" claim but incorrect "aud" claim
// In this case, something is returned by the clause, which returns a generic error // In this case, something is returned by the clause, which returns a generic error
{ {
@ -2392,7 +2538,11 @@ mod tests {
ALGORITHM HS512 KEY '{secret}' ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{ AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }}; 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 DURATION FOR SESSION 2h
; ;
@ -2450,7 +2600,11 @@ mod tests {
ALGORITHM HS512 KEY '{secret}' ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{ AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; 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
; ;
@ -2493,6 +2647,81 @@ 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()),
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;
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"));
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 // Test with correct "iss" claim but incorrect "aud" claim
{ {
let secret = "jwt_secret"; let secret = "jwt_secret";
@ -2517,7 +2746,11 @@ mod tests {
ALGORITHM HS512 KEY '{secret}' ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{ AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; 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; let res = token(&ds, &mut sess, &enc).await;
match res { 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!( res => panic!(
"Expected authentication to failed due to invalid token audience, but instead received: {:?}", "Expected authentication to failed due to invalid token audience, but instead received: {:?}",
res 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 // Test with correct "iss" claim but incorrect "aud" claim
// In this case, something is returned by the clause, which returns a generic error // In this case, something is returned by the clause, which returns a generic error
{ {
@ -2573,7 +2868,11 @@ mod tests {
ALGORITHM HS512 KEY '{secret}' ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{ AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }}; 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 DURATION FOR SESSION 2h
; ;
@ -2630,7 +2929,11 @@ mod tests {
ALGORITHM HS512 KEY '{secret}' ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{ AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; 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
; ;
@ -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 // Test with correct "iss" claim but incorrect "aud" claim
{ {
let secret = "jwt_secret"; let secret = "jwt_secret";
@ -2695,7 +3072,11 @@ mod tests {
ALGORITHM HS512 KEY '{secret}' ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{ AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; 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
; ;
@ -2718,7 +3099,7 @@ mod tests {
let res = token(&ds, &mut sess, &enc).await; let res = token(&ds, &mut sess, &enc).await;
match res { 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!( res => panic!(
"Expected authentication to failed due to invalid token audience, but instead received: {:?}", "Expected authentication to failed due to invalid token audience, but instead received: {:?}",
res 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 // Test with correct "iss" claim but incorrect "aud" claim
// In this case, something is returned by the clause, which returns a generic error // In this case, something is returned by the clause, which returns a generic error
{ {
@ -2750,7 +3191,11 @@ mod tests {
ALGORITHM HS512 KEY '{secret}' ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{ AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }}; 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 DURATION FOR SESSION 2h
; ;