Move AUTHENTICATE clause to DEFINE ACCESS statement root (#4385)

This commit is contained in:
Gerard Guillemas Martos 2024-07-30 16:23:54 +02:00 committed by GitHub
parent 63fcde0b80
commit d030c7d498
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 807 additions and 123 deletions

View file

@ -35,9 +35,9 @@ pub static EXPORT_BATCH_SIZE: Lazy<u32> = lazy_env_parse!("SURREAL_EXPORT_BATCH_
pub static MAX_STREAM_BATCH_SIZE: Lazy<u32> =
lazy_env_parse!("SURREAL_MAX_STREAM_BATCH_SIZE", u32, 1000);
/// Forward all signup/signin query errors to a client performing record access. Do not use in production.
pub static INSECURE_FORWARD_RECORD_ACCESS_ERRORS: Lazy<bool> =
lazy_env_parse!("SURREAL_INSECURE_FORWARD_RECORD_ACCESS_ERRORS", bool, false);
/// Forward all signup/signin/authenticate query errors to a client performing authentication. Do not use in production.
pub static INSECURE_FORWARD_ACCESS_ERRORS: Lazy<bool> =
lazy_env_parse!("SURREAL_INSECURE_FORWARD_ACCESS_ERRORS", bool, false);
#[cfg(any(
feature = "kv-mem",

View file

@ -1,6 +1,6 @@
use super::verify::{verify_db_creds, verify_ns_creds, verify_root_creds};
use super::{Actor, Level};
use crate::cnf::{INSECURE_FORWARD_RECORD_ACCESS_ERRORS, SERVER_NAME};
use crate::cnf::{INSECURE_FORWARD_ACCESS_ERRORS, SERVER_NAME};
use crate::dbs::Session;
use crate::err::Error;
use crate::iam::issue::{config, expiration};
@ -152,7 +152,7 @@ pub async fn db_access(
..Claims::default()
};
// AUTHENTICATE clause
if let Some(au) = at.authenticate {
if let Some(au) = &av.authenticate {
// Setup the system session for finding the signin record
let mut sess =
Session::editor().with_ns(&ns).with_db(&db);
@ -161,21 +161,21 @@ pub async fn db_access(
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
// Compute the value with the params
match kvs.evaluate(au, &sess, None).await {
Ok(val) => match val.record() {
match kvs.evaluate(au.clone(), &sess, None).await {
Ok(val) => match val.record() {
Some(id) => {
// Update rid with result from AUTHENTICATE clause
rid = id;
}
_ => return Err(Error::InvalidAuth),
},
Err(e) => {
return match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
_ => Err(Error::InvalidAuth),
Err(e) => return match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_ACCESS_ERRORS => {
Err(e)
}
}
_ => Err(Error::InvalidAuth),
},
}
}
// Log the authenticated access method info
@ -207,7 +207,7 @@ pub async fn db_access(
}
Err(e) => match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
e if *INSECURE_FORWARD_ACCESS_ERRORS => Err(e),
_ => Err(Error::AccessRecordSigninQueryFailed),
},
}

View file

@ -1,4 +1,4 @@
use crate::cnf::{INSECURE_FORWARD_RECORD_ACCESS_ERRORS, SERVER_NAME};
use crate::cnf::{INSECURE_FORWARD_ACCESS_ERRORS, SERVER_NAME};
use crate::dbs::Session;
use crate::err::Error;
use crate::iam::issue::{config, expiration};
@ -96,7 +96,7 @@ pub async fn db_access(
..Claims::default()
};
// AUTHENTICATE clause
if let Some(au) = at.authenticate {
if let Some(au) = &av.authenticate {
// Setup the system session for finding the signin record
let mut sess =
Session::editor().with_ns(&ns).with_db(&db);
@ -105,21 +105,21 @@ pub async fn db_access(
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
// Compute the value with the params
match kvs.evaluate(au, &sess, None).await {
Ok(val) => match val.record() {
match kvs.evaluate(au.clone(), &sess, None).await {
Ok(val) => match val.record() {
Some(id) => {
// Update rid with result from AUTHENTICATE clause
rid = id;
}
_ => return Err(Error::InvalidAuth),
},
Err(e) => {
return match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
_ => Err(Error::InvalidAuth),
Err(e) => return match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_ACCESS_ERRORS => {
Err(e)
}
}
_ => Err(Error::InvalidAuth),
},
}
}
// Log the authenticated access method info
@ -151,7 +151,7 @@ pub async fn db_access(
}
Err(e) => match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
e if *INSECURE_FORWARD_ACCESS_ERRORS => Err(e),
_ => Err(Error::AccessRecordSignupQueryFailed),
},
}

View file

@ -20,6 +20,10 @@ pub struct Claims {
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(alias = "ns")]
#[serde(alias = "NS")]
@ -70,6 +74,14 @@ impl From<Claims> for Value {
if let Some(iss) = v.iss {
out.insert("iss".to_string(), iss.into());
}
// Add sub field if set
if let Some(sub) = v.sub {
out.insert("sub".to_string(), sub.into());
}
// Add aud field if set
if let Some(aud) = v.aud {
out.insert("aud".to_string(), aud.into());
}
// Add iat field if set
if let Some(iat) = v.iat {
out.insert("iat".to_string(), iat.into());

View file

@ -1,4 +1,4 @@
use crate::cnf::INSECURE_FORWARD_RECORD_ACCESS_ERRORS;
use crate::cnf::INSECURE_FORWARD_ACCESS_ERRORS;
use crate::dbs::Session;
use crate::err::Error;
#[cfg(feature = "jwks")]
@ -6,7 +6,7 @@ use crate::iam::jwks;
use crate::iam::{issue::expiration, token::Claims, Actor, Auth, Level, Role};
use crate::kvs::{Datastore, LockType::*, TransactionType::*};
use crate::sql::access_type::{AccessType, JwtAccessVerify};
use crate::sql::{statements::DefineUserStatement, Algorithm, Value};
use crate::sql::{statements::DefineUserStatement, Algorithm, Thing, Value};
use crate::syn;
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use chrono::Utc;
@ -157,53 +157,33 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Ensure that the transaction is cancelled
tx.cancel().await?;
// Obtain the configuration to verify the token based on the access method
let (au, cf) = match de.kind.clone() {
AccessType::Record(at) => {
let cf = match at.jwt.verify.clone() {
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
#[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => {
if let Some(kid) = token_data.header.kid {
jwks::config(kvs, &kid, &jwks.url, token_data.header.alg).await
} else {
Err(Error::MissingTokenHeader("kid".to_string()))
}
let cf = match &de.kind {
AccessType::Record(at) => match &at.jwt.verify {
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
#[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => {
if let Some(kid) = token_data.header.kid {
jwks::config(kvs, &kid, &jwks.url, token_data.header.alg).await
} else {
Err(Error::MissingTokenHeader("kid".to_string()))
}
#[cfg(not(feature = "jwks"))]
_ => return Err(Error::AccessMethodMismatch),
}?;
(at.authenticate, cf)
}
}
#[cfg(not(feature = "jwks"))]
_ => return Err(Error::AccessMethodMismatch),
}?,
_ => return Err(Error::AccessMethodMismatch),
};
// Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?;
// AUTHENTICATE clause
if let Some(au) = au {
if let Some(au) = &de.authenticate {
// Setup the system session for finding the signin record
let mut sess = Session::editor().with_ns(&ns).with_db(&db);
sess.rd = Some(rid.clone().into());
sess.tk = Some(token_data.claims.clone().into());
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
// Compute the value with the params
match kvs.evaluate(au, &sess, None).await {
Ok(val) => match val.record() {
Some(id) => {
// Update rid with result from AUTHENTICATE clause
rid = id;
}
_ => return Err(Error::InvalidAuth),
},
Err(e) => {
return match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
_ => Err(Error::InvalidAuth),
}
}
}
rid = authenticate_record(kvs, &sess, au.clone()).await?;
}
// Log the success
debug!("Authenticated with record access method `{}`", ac);
@ -238,10 +218,10 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Ensure that the transaction is cancelled
tx.cancel().await?;
// Obtain the configuration to verify the token based on the access method
match de.kind.clone() {
match &de.kind {
// If the access type is Jwt, this is database access
AccessType::Jwt(at) => {
let cf = match at.verify {
let cf = match &at.verify {
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
#[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => {
@ -254,9 +234,17 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
#[cfg(not(feature = "jwks"))]
_ => return Err(Error::AccessMethodMismatch),
}?;
// Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?;
// AUTHENTICATE clause
if let Some(au) = &de.authenticate {
// Setup the system session for finding the signin record
let mut sess = Session::editor().with_ns(&ns).with_db(&db);
sess.tk = Some(token_data.claims.clone().into());
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
authenticate_jwt(kvs, &sess, au.clone()).await?;
}
// Parse the roles
let roles = match token_data.claims.roles {
// If no role is provided, grant the viewer role
@ -286,10 +274,10 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// If the access type is Record, this is record access
// Record access without an "id" claim is only possible if there is an AUTHENTICATE clause
// The clause can make up for the missing "id" claim by resolving other claims to a specific record
AccessType::Record(at) => match at.authenticate {
AccessType::Record(at) => match &de.authenticate {
Some(au) => {
trace!("Access method `{}` is record access with authenticate clause", ac);
let cf = match at.jwt.verify {
let cf = match &at.jwt.verify {
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
#[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => {
@ -311,21 +299,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
sess.tk = Some(token_data.claims.clone().into());
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
// Compute the value with the params
let rid = match kvs.evaluate(au, &sess, None).await {
Ok(val) => match val.record() {
// If found, return record identifier from AUTHENTICATE clause
Some(id) => id,
_ => return Err(Error::InvalidAuth),
},
Err(e) => {
return match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
_ => Err(Error::InvalidAuth),
}
}
};
let rid = authenticate_record(kvs, &sess, au.clone()).await?;
// Log the success
debug!("Authenticated with record access method `{}`", ac);
// Set the session
@ -397,8 +371,8 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Ensure that the transaction is cancelled
tx.cancel().await?;
// Obtain the configuration to verify the token based on the access method
let cf = match de.kind.clone() {
AccessType::Jwt(ac) => match ac.verify {
let cf = match &de.kind {
AccessType::Jwt(ac) => match &ac.verify {
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
#[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => {
@ -415,6 +389,15 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
}?;
// Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?;
// AUTHENTICATE clause
if let Some(au) = &de.authenticate {
// Setup the system session for finding the signin record
let mut sess = Session::editor().with_ns(&ns);
sess.tk = Some(token_data.claims.clone().into());
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
authenticate_jwt(kvs, &sess, au.clone()).await?;
}
// Parse the roles
let roles = match token_data.claims.roles {
// If no role is provided, grant the viewer role
@ -486,8 +469,8 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Ensure that the transaction is cancelled
tx.cancel().await?;
// Obtain the configuration to verify the token based on the access method
let cf = match de.kind.clone() {
AccessType::Jwt(ac) => match ac.verify {
let cf = match &de.kind {
AccessType::Jwt(ac) => match &ac.verify {
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
#[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => {
@ -504,6 +487,15 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
}?;
// Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?;
// AUTHENTICATE clause
if let Some(au) = &de.authenticate {
// Setup the system session for finding the signin record
let mut sess = Session::editor();
sess.tk = Some(token_data.claims.clone().into());
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
authenticate_jwt(kvs, &sess, au.clone()).await?;
}
// Parse the roles
let roles = match token_data.claims.roles {
// If no role is provided, grant the viewer role
@ -641,6 +633,52 @@ fn verify_pass(pass: &str, hash: &str) -> Result<(), Error> {
}
}
// Execute the AUTHENTICATE clause for a record access method
async fn authenticate_record(
kvs: &Datastore,
session: &Session,
authenticate: Value,
) -> Result<Thing, Error> {
match kvs.evaluate(authenticate, session, None).await {
Ok(val) => match val.record() {
// If the AUTHENTICATE clause returns a record, authentication continues with that record
Some(id) => Ok(id),
// If the AUTHENTICATE clause returns anything else, authentication fails generically
_ => Err(Error::InvalidAuth),
},
Err(e) => match e {
// If the AUTHENTICATE clause throws a specific error, authentication fails with that error
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_ACCESS_ERRORS => Err(e),
_ => Err(Error::InvalidAuth),
},
}
}
// Execute the AUTHENTICATE clause for a JWT access method
async fn authenticate_jwt(
kvs: &Datastore,
session: &Session,
authenticate: Value,
) -> Result<(), Error> {
match kvs.evaluate(authenticate, session, None).await {
Ok(val) => {
match val {
// If the AUTHENTICATE clause returns nothing, authentication continues
Value::None => Ok(()),
// If the AUTHENTICATE clause returns anything else, authentication fails generically
_ => Err(Error::InvalidAuth),
}
}
Err(e) => match e {
// If the AUTHENTICATE clause throws a specific error, authentication fails with that error
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_ACCESS_ERRORS => Err(e),
_ => Err(Error::InvalidAuth),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -1943,11 +1981,11 @@ mod tests {
format!(
r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
WITH JWT ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE (
-- Simple example increasing the record identifier by one
SELECT * FROM type::thing('user', meta::id($auth) + 1)
)
WITH JWT ALGORITHM HS512 KEY '{secret}'
DURATION FOR SESSION 2h
;
@ -2008,10 +2046,10 @@ mod tests {
SIGNIN (
SELECT * FROM type::thing('user', $id)
)
WITH JWT ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE (
SELECT id FROM user WHERE email = $token.email
)
WITH JWT ALGORITHM HS512 KEY '{secret}'
DURATION FOR SESSION 2h
;
@ -2096,6 +2134,7 @@ mod tests {
ds.execute(
format!(r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
WITH JWT ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{
-- Not just signin, this clause runs across signin, signup and authenticate, which makes it a nice place to centralize logic
IF !$auth.enabled {{
@ -2105,7 +2144,6 @@ mod tests {
-- Always need to return the user id back, otherwise auth generically fails
RETURN $auth;
}}
WITH JWT ALGORITHM HS512 KEY '{secret}'
DURATION FOR SESSION 2h
;
@ -2157,8 +2195,8 @@ mod tests {
format!(
r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
AUTHENTICATE {{}}
WITH JWT ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{}}
DURATION FOR SESSION 2h
;
@ -2190,4 +2228,547 @@ mod tests {
}
}
}
#[tokio::test]
async fn test_token_db_authenticate_clause() {
// Test with correct "iss" and "aud" claims
{
let secret = "jwt_secret";
let key = EncodingKey::from_secret(secret.as_ref());
let claims = Claims {
iss: Some("surrealdb-test".to_string()),
aud: Some("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 $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
}}
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" claim but incorrect "aud" claim
{
let secret = "jwt_secret";
let key = EncodingKey::from_secret(secret.as_ref());
let claims = Claims {
iss: Some("surrealdb-test".to_string()),
aud: Some("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 $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
}}
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" => {} // ok
res => panic!(
"Expected authentication to failed due to invalid token audience, 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
{
let secret = "jwt_secret";
let key = EncodingKey::from_secret(secret.as_ref());
let claims = Claims {
iss: Some("surrealdb-test".to_string()),
aud: Some("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" {{ RETURN "FAIL" }};
IF $token.aud != "surrealdb-test" {{ RETURN "FAIL" }};
}}
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::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
}
#[tokio::test]
async fn test_token_ns_authenticate_clause() {
// Test with correct "iss" and "aud" claims
{
let secret = "jwt_secret";
let key = EncodingKey::from_secret(secret.as_ref());
let claims = Claims {
iss: Some("surrealdb-test".to_string()),
aud: Some("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").with_db("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 $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
}}
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" claim but incorrect "aud" claim
{
let secret = "jwt_secret";
let key = EncodingKey::from_secret(secret.as_ref());
let claims = Claims {
iss: Some("surrealdb-test".to_string()),
aud: Some("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").with_db("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 $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
}}
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" => {} // ok
res => panic!(
"Expected authentication to failed due to invalid token audience, 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
{
let secret = "jwt_secret";
let key = EncodingKey::from_secret(secret.as_ref());
let claims = Claims {
iss: Some("surrealdb-test".to_string()),
aud: Some("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").with_db("test");
ds.execute(
format!(
r#"
DEFINE ACCESS user ON NAMESPACE TYPE JWT
ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }};
IF $token.aud != "surrealdb-test" {{ RETURN "FAIL" }};
}}
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::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
}
#[tokio::test]
async fn test_token_root_authenticate_clause() {
// Test with correct "iss" and "aud" claims
{
let secret = "jwt_secret";
let key = EncodingKey::from_secret(secret.as_ref());
let claims = Claims {
iss: Some("surrealdb-test".to_string()),
aud: Some("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().with_ns("test").with_db("test");
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 $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
}}
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.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";
let key = EncodingKey::from_secret(secret.as_ref());
let claims = Claims {
iss: Some("surrealdb-test".to_string()),
aud: Some("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().with_ns("test").with_db("test");
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 $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
}}
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" => {} // ok
res => panic!(
"Expected authentication to failed due to invalid token audience, 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
{
let secret = "jwt_secret";
let key = EncodingKey::from_secret(secret.as_ref());
let claims = Claims {
iss: Some("surrealdb-test".to_string()),
aud: Some("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().with_ns("test").with_db("test");
ds.execute(
format!(
r#"
DEFINE ACCESS user ON ROOT TYPE JWT
ALGORITHM HS512 KEY '{secret}'
AUTHENTICATE {{
IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }};
IF $token.aud != "surrealdb-test" {{ RETURN "FAIL" }};
}}
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::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
}
}

View file

@ -3,6 +3,7 @@ use crate::sql::statements::info::InfoStructure;
use crate::sql::statements::DefineAccessStatement;
use crate::sql::{escape::quote_str, Algorithm};
use revision::revisioned;
use revision::Error as RevisionError;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::Display;
@ -40,9 +41,6 @@ impl Display for AccessType {
if let Some(ref v) = ac.signin {
write!(f, " SIGNIN {v}")?
}
if let Some(ref v) = ac.authenticate {
write!(f, " AUTHENTICATE {v}")?
}
write!(f, " WITH JWT {}", ac.jwt)?;
}
}
@ -62,7 +60,6 @@ impl InfoStructure for AccessType {
"jwt".to_string() => v.jwt.structure(),
"signup".to_string(), if let Some(v) = v.signup => v.structure(),
"signin".to_string(), if let Some(v) = v.signin => v.structure(),
"authenticate".to_string(), if let Some(v) = v.authenticate => v.structure(),
}),
}
}
@ -260,26 +257,42 @@ pub struct JwtAccessVerifyJwks {
pub url: String,
}
#[revisioned(revision = 2)]
#[revisioned(revision = 3)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct RecordAccess {
pub signup: Option<Value>,
pub signin: Option<Value>,
pub jwt: JwtAccess,
#[revision(start = 2)]
// TODO(gguillemas): Field kept to gracefully handle breaking change.
// Remove when "revision" crate allows doing so.
#[revision(start = 2, end = 3, convert_fn = "authenticate_revision")]
pub authenticate: Option<Value>,
}
impl RecordAccess {
fn authenticate_revision(
&self,
_revision: u16,
_value: Option<Value>,
) -> Result<(), RevisionError> {
Err(RevisionError::Conversion(
"The \"AUTHENTICATE\" clause has been moved to \"DEFINE ACCESS\"".to_string(),
))
}
}
impl Default for RecordAccess {
fn default() -> Self {
Self {
signup: None,
signin: None,
authenticate: None,
jwt: JwtAccess {
..Default::default()
},
// TODO(gguillemas): Field kept to gracefully handle breaking change.
// Remove when "revision" crate allows doing so.
authenticate: None,
}
}
}

View file

@ -12,7 +12,7 @@ use revision::revisioned;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
#[revisioned(revision = 1)]
#[revisioned(revision = 2)]
#[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
@ -20,6 +20,8 @@ pub struct DefineAccessStatement {
pub name: Ident,
pub base: Base,
pub kind: AccessType,
#[revision(start = 2)]
pub authenticate: Option<Value>,
pub duration: AccessDuration,
pub comment: Option<Strand>,
pub if_not_exists: bool,
@ -168,6 +170,10 @@ impl Display for DefineAccessStatement {
}
// The specific access method definition is displayed by AccessType
write!(f, " {} ON {} TYPE {}", self.name, self.base, self.kind)?;
// The additional authentication clause
if let Some(ref v) = self.authenticate {
write!(f, " AUTHENTICATE {v}")?
}
// Always print relevant durations so defaults can be changed in the future
// If default values were not printed, exports would not be forward compatible
// None values need to be printed, as they are different from the default values
@ -212,6 +218,7 @@ impl InfoStructure for DefineAccessStatement {
Value::from(map! {
"name".to_string() => self.name.structure(),
"base".to_string() => self.base.structure(),
"authenticate".to_string(), if let Some(v) = self.authenticate => v.structure(),
"duration".to_string() => Value::from(map!{
"session".to_string() => self.duration.session.into(),
"grant".to_string(), if self.kind.can_issue_grants() => self.duration.grant.into(),

View file

@ -83,8 +83,10 @@ impl ser::Serializer for SerializerRecord {
pub struct SerializeRecord {
pub signup: Option<Value>,
pub signin: Option<Value>,
pub authenticate: Option<Value>,
pub jwt: JwtAccess,
// TODO(gguillemas): Field kept to gracefully handle breaking change.
// Remove when "revision" crate allows doing so.
pub authenticate: Option<Value>,
}
impl serde::ser::SerializeStruct for SerializeRecord {
@ -102,12 +104,14 @@ impl serde::ser::SerializeStruct for SerializeRecord {
"signin" => {
self.signin = value.serialize(ser::value::opt::Serializer.wrap())?;
}
"authenticate" => {
self.authenticate = value.serialize(ser::value::opt::Serializer.wrap())?;
}
"jwt" => {
self.jwt = value.serialize(SerializerJwt.wrap())?;
}
// TODO(gguillemas): Field kept to gracefully handle breaking change.
// Remove when "revision" crate allows doing so.
"authenticate" => {
self.authenticate = value.serialize(ser::value::opt::Serializer.wrap())?;
}
key => {
return Err(Error::custom(format!("unexpected field `RecordAccess::{key}`")));
}
@ -119,8 +123,10 @@ impl serde::ser::SerializeStruct for SerializeRecord {
Ok(RecordAccess {
signup: self.signup,
signin: self.signin,
authenticate: self.authenticate,
jwt: self.jwt,
// TODO(gguillemas): Field kept to gracefully handle breaking change.
// Remove when "revision" crate allows doing so.
authenticate: None,
})
}
}

View file

@ -7,6 +7,7 @@ use crate::sql::Base;
use crate::sql::Duration;
use crate::sql::Ident;
use crate::sql::Strand;
use crate::sql::Value;
use ser::Serializer as _;
use serde::ser::Error as _;
use serde::ser::Impossible;
@ -45,6 +46,7 @@ pub struct SerializeDefineAccessStatement {
name: Ident,
base: Base,
kind: AccessType,
authenticate: Option<Value>,
duration: AccessDuration,
comment: Option<Strand>,
if_not_exists: bool,
@ -68,6 +70,9 @@ impl serde::ser::SerializeStruct for SerializeDefineAccessStatement {
"kind" => {
self.kind = value.serialize(ser::access_type::Serializer.wrap())?;
}
"authenticate" => {
self.authenticate = value.serialize(ser::value::opt::Serializer.wrap())?;
}
"duration" => {
self.duration = value.serialize(SerializerDuration.wrap())?;
}
@ -91,6 +96,7 @@ impl serde::ser::SerializeStruct for SerializeDefineAccessStatement {
name: self.name,
base: self.base,
kind: self.kind,
authenticate: self.authenticate,
duration: self.duration,
comment: self.comment,
if_not_exists: self.if_not_exists,

View file

@ -310,11 +310,6 @@ impl Parser<'_> {
ac.signin =
Some(stk.run(|stk| self.parse_value(stk)).await?);
}
t!("AUTHENTICATE") => {
self.pop_peek();
ac.authenticate =
Some(stk.run(|stk| self.parse_value(stk)).await?);
}
_ => break,
}
}
@ -327,6 +322,10 @@ impl Parser<'_> {
_ => break,
}
}
t!("AUTHENTICATE") => {
self.pop_peek();
res.authenticate = Some(stk.run(|stk| self.parse_value(stk)).await?);
}
t!("DURATION") => {
self.pop_peek();
while self.eat(t!("FOR")) {
@ -528,10 +527,6 @@ impl Parser<'_> {
self.pop_peek();
ac.signin = Some(stk.run(|stk| self.parse_value(stk)).await?);
}
t!("AUTHENTICATE") => {
self.pop_peek();
ac.authenticate = Some(stk.run(|stk| self.parse_value(stk)).await?);
}
_ => break,
}
}

View file

@ -366,6 +366,7 @@ fn parse_define_token() {
}),
issue: None,
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
@ -441,6 +442,7 @@ fn parse_define_token_jwks() {
}),
issue: None,
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
@ -566,6 +568,7 @@ fn parse_define_access_jwt_key() {
}),
issue: None,
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
@ -599,6 +602,41 @@ fn parse_define_access_jwt_key() {
key: "bar".to_string(),
}),
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
token: Some(Duration::from_hours(1)),
session: None,
},
comment: None,
if_not_exists: false,
})),
)
}
// Asymmetric verify and issue with authenticate clause.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM EDDSA KEY "foo" WITH ISSUER KEY "bar" AUTHENTICATE true"#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::EdDSA,
key: "foo".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::EdDSA,
key: "bar".to_string(),
}),
}),
authenticate: Some(Value::Bool(true)),
// Default durations.
duration: AccessDuration {
grant: None,
@ -632,6 +670,7 @@ fn parse_define_access_jwt_key() {
key: "foo".to_string(),
}),
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
@ -665,6 +704,7 @@ fn parse_define_access_jwt_key() {
key: "foo".to_string(),
}),
}),
authenticate: None,
duration: AccessDuration {
grant: None,
token: Some(Duration::from_secs(10)),
@ -697,6 +737,7 @@ fn parse_define_access_jwt_key() {
key: "foo".to_string(),
}),
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
@ -775,6 +816,7 @@ fn parse_define_access_jwt_key() {
}),
issue: None,
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
@ -805,6 +847,7 @@ fn parse_define_access_jwt_key() {
}),
issue: None,
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
@ -838,6 +881,7 @@ fn parse_define_access_jwt_jwks() {
}),
issue: None,
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
@ -870,6 +914,7 @@ fn parse_define_access_jwt_jwks() {
key: "foo".to_string(),
}),
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
@ -902,6 +947,7 @@ fn parse_define_access_jwt_jwks() {
key: "foo".to_string(),
}),
}),
authenticate: None,
duration: AccessDuration {
grant: None,
token: Some(Duration::from_secs(10)),
@ -933,6 +979,7 @@ fn parse_define_access_jwt_jwks() {
key: "foo".to_string(),
}),
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,
@ -965,6 +1012,7 @@ fn parse_define_access_jwt_jwks() {
key: "foo".to_string(),
}),
}),
authenticate: None,
duration: AccessDuration {
grant: None,
token: Some(Duration::from_secs(10)),
@ -992,6 +1040,7 @@ fn parse_define_access_record() {
assert_eq!(stmt.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db);
assert_eq!(stmt.authenticate, None);
assert_eq!(
stmt.duration,
// Default durations.
@ -1023,7 +1072,7 @@ fn parse_define_access_record() {
_ => panic!(),
}
}
// Session duration, signing and authentication queries are explicitly defined.
// Session duration, signing and authenticate clauses are explicitly defined.
{
let res = test_parse!(
parse_stmt,
@ -1039,6 +1088,7 @@ fn parse_define_access_record() {
assert_eq!(stmt.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db);
assert_eq!(stmt.authenticate, Some(Value::Bool(true)));
assert_eq!(
stmt.duration,
AccessDuration {
@ -1053,7 +1103,6 @@ fn parse_define_access_record() {
AccessType::Record(ac) => {
assert_eq!(ac.signup, Some(Value::Bool(true)));
assert_eq!(ac.signin, Some(Value::Bool(false)));
assert_eq!(ac.authenticate, Some(Value::Bool(true)));
match ac.jwt.verify {
JwtAccessVerify::Key(key) => {
assert_eq!(key.alg, Algorithm::Hs512);
@ -1085,7 +1134,6 @@ fn parse_define_access_record() {
kind: AccessType::Record(RecordAccess {
signup: None,
signin: None,
authenticate: None,
jwt: JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::Hs384,
@ -1096,8 +1144,12 @@ fn parse_define_access_record() {
// Issuer key matches verification key by default in symmetric algorithms.
key: "foo".to_string(),
}),
}
},
// TODO(gguillemas): Field kept to gracefully handle breaking change.
// Remove when "revision" crate allows doing so.
authenticate: None
}),
authenticate: None,
duration: AccessDuration {
grant: None,
token: Some(Duration::from_secs(10)),
@ -1123,7 +1175,6 @@ fn parse_define_access_record() {
kind: AccessType::Record(RecordAccess {
signup: None,
signin: None,
authenticate: None,
jwt: JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::Ps512,
@ -1133,8 +1184,12 @@ fn parse_define_access_record() {
alg: Algorithm::Ps512,
key: "bar".to_string(),
}),
}
},
// TODO(gguillemas): Field kept to gracefully handle breaking change.
// Remove when "revision" crate allows doing so.
authenticate: None
}),
authenticate: None,
duration: AccessDuration {
grant: None,
token: Some(Duration::from_secs(10)),
@ -1160,7 +1215,6 @@ fn parse_define_access_record() {
kind: AccessType::Record(RecordAccess {
signup: None,
signin: None,
authenticate: None,
jwt: JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::Rs256,
@ -1170,8 +1224,12 @@ fn parse_define_access_record() {
alg: Algorithm::Rs256,
key: "bar".to_string(),
}),
}
},
// TODO(gguillemas): Field kept to gracefully handle breaking change.
// Remove when "revision" crate allows doing so.
authenticate: None
}),
authenticate: None,
duration: AccessDuration {
grant: None,
token: Some(Duration::from_secs(10)),
@ -1231,15 +1289,18 @@ fn parse_define_access_record_with_jwt() {
kind: AccessType::Record(RecordAccess {
signup: None,
signin: None,
authenticate: None,
jwt: JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::EdDSA,
key: "foo".to_string(),
}),
issue: None,
}
},
// TODO(gguillemas): Field kept to gracefully handle breaking change.
// Remove when "revision" crate allows doing so.
authenticate: None
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,

View file

@ -200,7 +200,6 @@ fn statements() -> Vec<Statement> {
kind: AccessType::Record(RecordAccess {
signup: None,
signin: None,
authenticate: None,
jwt: JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::EdDSA,
@ -208,7 +207,11 @@ fn statements() -> Vec<Statement> {
}),
issue: None,
},
// TODO(gguillemas): Field kept to gracefully handle breaking change.
// Remove when "revision" crate allows doing so.
authenticate: None,
}),
authenticate: None,
// Default durations.
duration: AccessDuration {
grant: None,