Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion crates/application/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2806,7 +2806,26 @@ impl<RT: Runtime> Application<RT> {
)
.await;

Identity::user(identity_result?)
match identity_result {
Ok(user_identity) => Identity::user(user_identity),
Err(error) => {
// For authentication errors, store them in Identity::Unknown so that
// getUserIdentityDebug can access and log them instead of failing the
// request
if let Some(error_metadata) = error.downcast_ref::<ErrorMetadata>() {
if matches!(error_metadata.code, errors::ErrorCode::Unauthenticated) {
return Ok(Identity::Unknown(Some(error_metadata.clone())));
}
}
// For other errors (non-authentication), propagate them normally
return Err(error);
},
}
},
AuthenticationToken::PlaintextUser(token) => {
// For plaintext authentication, create a PlaintextUser identity
// The server is responsible for validating the token
Identity::PlaintextUser(token)
},
AuthenticationToken::None => Identity::Unknown(None),
};
Expand Down
1 change: 1 addition & 0 deletions crates/authentication/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub fn token_to_authorization_header(token: AuthenticationToken) -> anyhow::Resu
None => Ok(Some(format!("Convex {key}"))),
},
AuthenticationToken::User(key) => Ok(Some(format!("Bearer {key}"))),
AuthenticationToken::PlaintextUser(key) => Ok(Some(format!("Bearer {key}"))),
AuthenticationToken::None => Ok(None),
}
}
Expand Down
13 changes: 13 additions & 0 deletions crates/convex/sync_types/src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ enum AuthenticationTokenJson {
User {
value: String,
},
PlaintextUser {
value: String,
},
None,
}

Expand Down Expand Up @@ -282,6 +285,13 @@ impl TryFrom<ClientMessage> for JsonValue {
base_version,
token: AuthenticationTokenJson::User { value },
},
ClientMessage::Authenticate {
base_version,
token: AuthenticationToken::PlaintextUser(value),
} => ClientMessageJson::Authenticate {
base_version,
token: AuthenticationTokenJson::PlaintextUser { value },
},
ClientMessage::Authenticate {
base_version,
token: AuthenticationToken::None,
Expand Down Expand Up @@ -374,6 +384,9 @@ impl TryFrom<JsonValue> for ClientMessage {
)
},
AuthenticationTokenJson::User { value } => AuthenticationToken::User(value),
AuthenticationTokenJson::PlaintextUser { value } => {
AuthenticationToken::PlaintextUser(value)
},
AuthenticationTokenJson::None => AuthenticationToken::None,
},
},
Expand Down
2 changes: 2 additions & 0 deletions crates/convex/sync_types/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ pub enum AuthenticationToken {
Admin(String, Option<UserIdentityAttributes>),
/// OpenID Connect JWT
User(String),
/// Plaintext authentication token (no JWT validation)
PlaintextUser(String),
#[default]
/// Logged out.
None,
Expand Down
60 changes: 60 additions & 0 deletions crates/isolate/src/environment/udf/async_syscall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,12 @@ impl<RT: Runtime, P: AsyncSyscallProvider<RT>> DatabaseSyscallsV1<RT, P> {
"1.0/getUserIdentity" => {
Box::pin(Self::get_user_identity(provider, args)).await
},
"1.0/getUserIdentityDebug" => {
Box::pin(Self::get_user_identity_debug(provider, args)).await
},
"1.0/getUserIdentityInsecure" => {
Box::pin(Self::get_user_identity_insecure(provider, args)).await
},
// Storage
"1.0/storageDelete" => Box::pin(Self::storage_delete(provider, args)).await,
"1.0/storageGetMetadata" => {
Expand Down Expand Up @@ -788,6 +794,60 @@ impl<RT: Runtime, P: AsyncSyscallProvider<RT>> DatabaseSyscallsV1<RT, P> {
Ok(JsonValue::Null)
}

#[convex_macro::instrument_future]
async fn get_user_identity_debug(
provider: &mut P,
_args: JsonValue,
) -> anyhow::Result<JsonValue> {
provider.observe_identity()?;
let component = provider.component()?;
let tx = provider.tx()?;
let user_identity = tx.user_identity();

if !component.is_root() {
log_component_get_user_identity(user_identity.is_some());
}

// If we have a valid user identity, return it
if let Some(user_identity) = user_identity {
return user_identity.try_into();
}

// If no user identity, check if we have error details from JWT validation
let identity = tx.identity();
if let keybroker::Identity::Unknown(Some(error_metadata)) = identity {
// Create a structured error response with details for debugging
let error_response = json!({
"error": {
"code": error_metadata.short_msg,
"message": error_metadata.msg,
"details": "JWT validation failed. Check your token format, expiration, issuer, and audience claims."
}
});
return Ok(error_response);
}

// No identity provided (not an error case)
Ok(JsonValue::Null)
}

#[convex_macro::instrument_future]
async fn get_user_identity_insecure(
provider: &mut P,
_args: JsonValue,
) -> anyhow::Result<JsonValue> {
let tx = provider.tx()?;
let identity = tx.identity();

// Return the plaintext token if this is a PlaintextUser identity
if let keybroker::Identity::PlaintextUser(token) = identity {
return Ok(JsonValue::String(token.clone()));
}

// Return null for any other identity type (including regular User identities)
Ok(JsonValue::Null)
}

#[convex_macro::instrument_future]
async fn storage_generate_upload_url(
provider: &mut P,
Expand Down
246 changes: 246 additions & 0 deletions crates/isolate/src/tests/auth_debug.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use common::{
assert_obj,
types::MemberId,
value::ConvexValue,
};
use keybroker::{
testing::TestUserIdentity,
AdminIdentity,
Identity,
UserIdentity,
};
use must_let::must_let;
use runtime::testing::TestRuntime;

use crate::test_helpers::UdfTest;

#[convex_macro::test_runtime]
async fn test_get_user_identity_debug_with_plaintext_user(rt: TestRuntime) -> anyhow::Result<()> {
UdfTest::run_test_with_isolate2(rt, async move |t| {
// Test that getUserIdentityDebug works with regular user identity
let identity = Identity::user(UserIdentity::test());
let (result, outcome) = t
.query_outcome("auth:getUserIdentityDebug", assert_obj!(), identity.clone())
.await?;

// Should return the user identity, not an error
must_let!(let ConvexValue::Object(obj) = result);
assert!(obj.get("name").is_some());
assert!(outcome.observed_identity);

// Test with PlaintextUser identity - should return null (no JWT to debug)
let plaintext_identity = Identity::PlaintextUser("test-plaintext-token".to_string());
let (result, outcome) = t
.query_outcome(
"auth:getUserIdentityDebug",
assert_obj!(),
plaintext_identity,
)
.await?;

assert_eq!(result, ConvexValue::Null);
assert!(outcome.observed_identity);

Ok(())
})
.await
}

#[convex_macro::test_runtime]
async fn test_get_user_identity_insecure_with_different_identities(
rt: TestRuntime,
) -> anyhow::Result<()> {
UdfTest::run_test_with_isolate2(rt, async move |t| {
// Test with PlaintextUser identity - should return the plaintext token
let plaintext_token = "my-test-plaintext-token-12345";
let plaintext_identity = Identity::PlaintextUser(plaintext_token.to_string());
let (result, outcome) = t
.query_outcome(
"auth:getUserIdentityInsecure",
assert_obj!(),
plaintext_identity,
)
.await?;

must_let!(let ConvexValue::String(token) = result);
assert_eq!(&*token, plaintext_token);
assert!(outcome.observed_identity == false);

// Test with regular User identity - should return null
let user_identity = Identity::user(UserIdentity::test());
let (result, outcome) = t
.query_outcome("auth:getUserIdentityInsecure", assert_obj!(), user_identity)
.await?;

assert_eq!(result, ConvexValue::Null);
assert!(outcome.observed_identity == false);

// Test with System identity - should return null
let system_identity = Identity::system();
let (result, outcome) = t
.query_outcome(
"auth:getUserIdentityInsecure",
assert_obj!(),
system_identity,
)
.await?;

assert_eq!(result, ConvexValue::Null);
assert!(outcome.observed_identity == false);

// Test with Admin identity - should return null
let admin_identity = Identity::InstanceAdmin(AdminIdentity::new_for_test_only(
"test-admin-key".to_string(),
MemberId(1),
));
let (result, outcome) = t
.query_outcome(
"auth:getUserIdentityInsecure",
assert_obj!(),
admin_identity,
)
.await?;

assert_eq!(result, ConvexValue::Null);
assert!(outcome.observed_identity == false);

// Test with Unknown identity - should return null
let unknown_identity = Identity::Unknown(None);
let (result, outcome) = t
.query_outcome(
"auth:getUserIdentityInsecure",
assert_obj!(),
unknown_identity,
)
.await?;

assert_eq!(result, ConvexValue::Null);
assert!(outcome.observed_identity == false);

Ok(())
})
.await
}

#[convex_macro::test_runtime]
async fn test_plaintext_user_admin_access_restriction(rt: TestRuntime) -> anyhow::Result<()> {
UdfTest::run_test_with_isolate2(rt, async move |t| {
// Test that PlaintextUser identity cannot access admin-protected functions
let plaintext_identity = Identity::PlaintextUser("admin-wannabe-token".to_string());

// This test would verify that PlaintextUser identities are properly rejected
// by the must_be_admin_internal function changes
let (outcome, _token) = t
.raw_query(
"auth:testAdminAccess",
vec![ConvexValue::Object(assert_obj!())],
plaintext_identity,
None,
)
.await?;

// Should fail with admin access error
assert!(outcome.result.is_err());
let error = outcome.result.unwrap_err();
let error_str = error.to_string();
assert!(error_str.contains("BadDeployKey") || error_str.contains("invalid"));

// Compare with regular admin identity which should succeed
let admin_identity = Identity::InstanceAdmin(AdminIdentity::new_for_test_only(
"valid-admin-key".to_string(),
MemberId(1),
));

// This should succeed for admin identities
let (admin_outcome, _token) = t
.raw_query(
"auth:testAdminAccess",
vec![ConvexValue::Object(assert_obj!())],
admin_identity,
None,
)
.await?;

// Admin should have access
assert!(admin_outcome.result.is_ok());

Ok(())
})
.await
}

#[convex_macro::test_runtime]
async fn test_plaintext_user_identity_creation_and_handling(rt: TestRuntime) -> anyhow::Result<()> {
UdfTest::run_test_with_isolate2(rt, async move |t| {
let test_token = "test-plaintext-auth-token-xyz";
let plaintext_identity = Identity::PlaintextUser(test_token.to_string());

// Test that PlaintextUser identity is properly handled in queries
let (result, outcome) = t
.query_outcome(
"auth:getIdentityType",
assert_obj!(),
plaintext_identity.clone(),
)
.await?;

// Should indicate it's a PlaintextUser identity
must_let!(let ConvexValue::String(identity_type) = result);
assert_eq!(&*identity_type, "PlaintextUser");
assert!(outcome.observed_identity);

// Test that getUserIdentityInsecure returns the correct token
let (token_result, _) = t
.query_outcome(
"auth:getUserIdentityInsecure",
assert_obj!(),
plaintext_identity,
)
.await?;

must_let!(let ConvexValue::String(returned_token) = token_result);
assert_eq!(&*returned_token, test_token);

Ok(())
})
.await
}

#[convex_macro::test_runtime]
async fn test_get_user_identity_debug_error_scenarios(rt: TestRuntime) -> anyhow::Result<()> {
UdfTest::run_test_with_isolate2(rt, async move |t| {
// Test getUserIdentityDebug with Unknown identity containing error
let error_message = "JWT validation failed: token expired";
let unknown_identity_with_error = Identity::Unknown(Some(
errors::ErrorMetadata::bad_request("InvalidJWT", error_message),
));

let (result, outcome) = t
.query_outcome(
"auth:getUserIdentityDebug",
assert_obj!(),
unknown_identity_with_error,
)
.await?;

// Should return structured error information
must_let!(let ConvexValue::Object(error_obj) = result);
assert!(error_obj.get("error").is_some());
must_let!(let ConvexValue::Object(error_obj_inner) = error_obj.get("error").unwrap());
assert!(error_obj_inner.get("code").is_some());
assert!(error_obj_inner.get("message").is_some());
assert!(outcome.observed_identity);

// Test with Unknown identity without error - should return null
let unknown_identity = Identity::Unknown(None);
let (result, outcome) = t
.query_outcome("auth:getUserIdentityDebug", assert_obj!(), unknown_identity)
.await?;

assert_eq!(result, ConvexValue::Null);
assert!(outcome.observed_identity);

Ok(())
})
.await
}
Loading
Loading