Skip to content

Commit 5e877b2

Browse files
committed
Add JWT authentication debugging and plaintext auth support
This commit introduces comprehensive JWT debugging capabilities and experimental plaintext authentication support to help developers troubleshoot authentication issues. **New Features:** - getUserIdentityDebug() method returns detailed error information when JWT validation fails - getUserIdentityInsecure() method for accessing raw plaintext tokens (development/debugging only) - setAuthInsecure() client method for plaintext authentication mode **Backend Implementation:** - New 1.0/getUserIdentityDebug and 1.0/getUserIdentityInsecure syscalls in Rust - PlaintextUser identity type added to authentication system - Enhanced error metadata for JWT validation failures **Frontend Integration:** - TypeScript Auth interface extended with new debug methods - React client support for plaintext authentication mode - WebSocket protocol updates for plaintext token handling **Backward Compatibility:** - Existing getUserIdentity() method unchanged - All current authentication flows continue to work - New features are opt-in and don't affect existing implementations This implementation enables developers to get detailed JWT error messages instead of generic authentication failures, significantly improving the debugging experience for authentication issues.
1 parent 342bfd2 commit 5e877b2

File tree

18 files changed

+302
-13
lines changed

18 files changed

+302
-13
lines changed

crates/application/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2808,6 +2808,11 @@ impl<RT: Runtime> Application<RT> {
28082808

28092809
Identity::user(identity_result?)
28102810
},
2811+
AuthenticationToken::PlaintextUser(token) => {
2812+
// For plaintext authentication, create a PlaintextUser identity
2813+
// The server is responsible for validating the token
2814+
Identity::PlaintextUser(token)
2815+
},
28112816
AuthenticationToken::None => Identity::Unknown(None),
28122817
};
28132818
Ok(identity)

crates/authentication/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ pub fn token_to_authorization_header(token: AuthenticationToken) -> anyhow::Resu
131131
None => Ok(Some(format!("Convex {key}"))),
132132
},
133133
AuthenticationToken::User(key) => Ok(Some(format!("Bearer {key}"))),
134+
AuthenticationToken::PlaintextUser(key) => Ok(Some(format!("Bearer {key}"))),
134135
AuthenticationToken::None => Ok(None),
135136
}
136137
}

crates/convex/sync_types/src/json.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ enum AuthenticationTokenJson {
155155
User {
156156
value: String,
157157
},
158+
PlaintextUser {
159+
value: String,
160+
},
158161
None,
159162
}
160163

@@ -282,6 +285,13 @@ impl TryFrom<ClientMessage> for JsonValue {
282285
base_version,
283286
token: AuthenticationTokenJson::User { value },
284287
},
288+
ClientMessage::Authenticate {
289+
base_version,
290+
token: AuthenticationToken::PlaintextUser(value),
291+
} => ClientMessageJson::Authenticate {
292+
base_version,
293+
token: AuthenticationTokenJson::PlaintextUser { value },
294+
},
285295
ClientMessage::Authenticate {
286296
base_version,
287297
token: AuthenticationToken::None,
@@ -374,6 +384,9 @@ impl TryFrom<JsonValue> for ClientMessage {
374384
)
375385
},
376386
AuthenticationTokenJson::User { value } => AuthenticationToken::User(value),
387+
AuthenticationTokenJson::PlaintextUser { value } => {
388+
AuthenticationToken::PlaintextUser(value)
389+
},
377390
AuthenticationTokenJson::None => AuthenticationToken::None,
378391
},
379392
},

crates/convex/sync_types/src/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ pub enum AuthenticationToken {
252252
Admin(String, Option<UserIdentityAttributes>),
253253
/// OpenID Connect JWT
254254
User(String),
255+
/// Plaintext authentication token (no JWT validation)
256+
PlaintextUser(String),
255257
#[default]
256258
/// Logged out.
257259
None,

crates/isolate/src/environment/udf/async_syscall.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,12 @@ impl<RT: Runtime, P: AsyncSyscallProvider<RT>> DatabaseSyscallsV1<RT, P> {
696696
"1.0/getUserIdentity" => {
697697
Box::pin(Self::get_user_identity(provider, args)).await
698698
},
699+
"1.0/getUserIdentityDebug" => {
700+
Box::pin(Self::get_user_identity_debug(provider, args)).await
701+
},
702+
"1.0/getUserIdentityInsecure" => {
703+
Box::pin(Self::get_user_identity_insecure(provider, args)).await
704+
},
699705
// Storage
700706
"1.0/storageDelete" => Box::pin(Self::storage_delete(provider, args)).await,
701707
"1.0/storageGetMetadata" => {
@@ -788,6 +794,60 @@ impl<RT: Runtime, P: AsyncSyscallProvider<RT>> DatabaseSyscallsV1<RT, P> {
788794
Ok(JsonValue::Null)
789795
}
790796

797+
#[convex_macro::instrument_future]
798+
async fn get_user_identity_debug(
799+
provider: &mut P,
800+
_args: JsonValue,
801+
) -> anyhow::Result<JsonValue> {
802+
provider.observe_identity()?;
803+
let component = provider.component()?;
804+
let tx = provider.tx()?;
805+
let user_identity = tx.user_identity();
806+
807+
if !component.is_root() {
808+
log_component_get_user_identity(user_identity.is_some());
809+
}
810+
811+
// If we have a valid user identity, return it
812+
if let Some(user_identity) = user_identity {
813+
return user_identity.try_into();
814+
}
815+
816+
// If no user identity, check if we have error details from JWT validation
817+
let identity = tx.identity();
818+
if let keybroker::Identity::Unknown(Some(error_metadata)) = identity {
819+
// Create a structured error response with details for debugging
820+
let error_response = json!({
821+
"error": {
822+
"code": error_metadata.short_msg,
823+
"message": error_metadata.msg,
824+
"details": "JWT validation failed. Check your token format, expiration, issuer, and audience claims."
825+
}
826+
});
827+
return Ok(error_response);
828+
}
829+
830+
// No identity provided (not an error case)
831+
Ok(JsonValue::Null)
832+
}
833+
834+
#[convex_macro::instrument_future]
835+
async fn get_user_identity_insecure(
836+
provider: &mut P,
837+
_args: JsonValue,
838+
) -> anyhow::Result<JsonValue> {
839+
let tx = provider.tx()?;
840+
let identity = tx.identity();
841+
842+
// Return the plaintext token if this is a PlaintextUser identity
843+
if let keybroker::Identity::PlaintextUser(token) = identity {
844+
return Ok(JsonValue::String(token.clone()));
845+
}
846+
847+
// Return null for any other identity type (including regular User identities)
848+
Ok(JsonValue::Null)
849+
}
850+
791851
#[convex_macro::instrument_future]
792852
async fn storage_generate_upload_url(
793853
provider: &mut P,

crates/keybroker/src/broker.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ use pb::{
6565
convex_identity::{
6666
unchecked_identity::Identity as UncheckedIdentityProto,
6767
ActingUser,
68+
PlaintextUserIdentity,
6869
UnknownIdentity,
6970
},
7071
convex_keys::{
@@ -144,6 +145,8 @@ pub enum Identity {
144145
// ActingUser keeps track of the ID of the admin acting as a user,
145146
// and that user's fake attributes
146147
ActingUser(AdminIdentity, UserIdentityAttributes),
148+
// PlaintextUser holds a plaintext authentication token for server-side validation
149+
PlaintextUser(String),
147150
// Unknown(None) means no identity was provided.
148151
// Unknown(Some(error_message)) means an error occurred while parsing the identity.
149152
// We allow the request to go through, but keep the error to throw when code tries to
@@ -159,6 +162,7 @@ impl From<Identity> for AuthenticationToken {
159162
AuthenticationToken::Admin(identity.key, Some(user))
160163
},
161164
Identity::InstanceAdmin(identity) => AuthenticationToken::Admin(identity.key, None),
165+
Identity::PlaintextUser(token) => AuthenticationToken::PlaintextUser(token),
162166
_ => AuthenticationToken::None,
163167
}
164168
}
@@ -180,6 +184,11 @@ impl From<Identity> for pb::convex_identity::UncheckedIdentity {
180184
attributes: Some(attributes.into()),
181185
})
182186
},
187+
Identity::PlaintextUser(token) => {
188+
UncheckedIdentityProto::PlaintextUserIdentity(PlaintextUserIdentity {
189+
token: Some(token),
190+
})
191+
},
183192
Identity::Unknown(error_message) => UncheckedIdentityProto::Unknown(UnknownIdentity {
184193
error_message: error_message.map(|e| e.into()),
185194
}),
@@ -216,6 +225,10 @@ impl Identity {
216225
attributes.ok_or_else(|| anyhow::anyhow!("Missing user attributes"))?;
217226
Ok(Identity::ActingUser(admin_identity, attributes.try_into()?))
218227
},
228+
UncheckedIdentityProto::PlaintextUserIdentity(PlaintextUserIdentity { token }) => {
229+
let token = token.ok_or_else(|| anyhow::anyhow!("Missing plaintext token"))?;
230+
Ok(Identity::PlaintextUser(token))
231+
},
219232
UncheckedIdentityProto::Unknown(UnknownIdentity { error_message }) => Ok(
220233
Identity::Unknown(error_message.map(|e| e.try_into()).transpose()?),
221234
),
@@ -252,6 +265,7 @@ impl From<Identity> for InertIdentity {
252265
Identity::InstanceAdmin(i) => InertIdentity::InstanceAdmin(i.instance_name),
253266
Identity::System(_) => InertIdentity::System,
254267
Identity::Unknown(_) => InertIdentity::Unknown,
268+
Identity::PlaintextUser(_) => InertIdentity::Unknown,
255269
Identity::User(user) => InertIdentity::User(user.attributes.token_identifier),
256270
Identity::ActingUser(identity, user) => match identity.principal {
257271
AdminIdentityPrincipal::Member(member_id) => {
@@ -273,6 +287,7 @@ impl PartialEq for Identity {
273287
(Self::User(l), Self::User(r)) => {
274288
l.attributes.token_identifier == r.attributes.token_identifier
275289
},
290+
(Self::PlaintextUser(l), Self::PlaintextUser(r)) => l == r,
276291
(Self::Unknown(_), Self::Unknown(_)) => true,
277292
(
278293
Self::ActingUser(l_admin_identity, l_attributes),
@@ -281,6 +296,7 @@ impl PartialEq for Identity {
281296
(Self::InstanceAdmin(_), _)
282297
| (Self::System(_), _)
283298
| (Self::User(_), _)
299+
| (Self::PlaintextUser(_), _)
284300
| (Self::Unknown(_), _)
285301
| (Self::ActingUser(..), _) => false,
286302
}
@@ -297,6 +313,7 @@ impl Identity {
297313
Identity::Unknown(error_message) => {
298314
IdentityCacheKey::Unknown(error_message.map(|e| e.to_string()))
299315
},
316+
Identity::PlaintextUser(_) => IdentityCacheKey::Unknown(None),
300317
Identity::User(user) => IdentityCacheKey::User(user.attributes),
301318
// Identity of the impersonator not relevant for caching. Only the one being
302319
// impersonated.

crates/local_backend/src/admin.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ fn must_be_admin_internal(
6767
let admin_identity = match identity {
6868
Identity::InstanceAdmin(admin_identity) => admin_identity,
6969
Identity::ActingUser(admin_identity, _user_identity_attributes) => admin_identity,
70-
Identity::System(_) | Identity::User(_) | Identity::Unknown(_) => {
70+
Identity::System(_)
71+
| Identity::User(_)
72+
| Identity::PlaintextUser(_)
73+
| Identity::Unknown(_) => {
7174
return Err(bad_admin_key_error(identity.instance_name()).into());
7275
},
7376
};

crates/pb/protos/convex_identity.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ message AuthenticationToken {
99
oneof identity {
1010
AdminAuthenticationToken admin = 1;
1111
string user = 2;
12+
string plaintext_user = 4;
1213
google.protobuf.Empty none = 3;
1314
}
1415
}
@@ -26,13 +27,18 @@ message UncheckedIdentity {
2627
UserIdentity user_identity = 3;
2728
ActingUser acting_user = 4;
2829
UnknownIdentity unknown = 5;
30+
PlaintextUserIdentity plaintext_user_identity = 6;
2931
}
3032
}
3133

3234
message UnknownIdentity {
3335
optional errors.ErrorMetadata error_message = 1;
3436
}
3537

38+
message PlaintextUserIdentity {
39+
optional string token = 1;
40+
}
41+
3642
message AdminIdentity {
3743
optional string instance_name = 1;
3844
optional string key = 3;

crates/pb/src/authentication_token.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ impl TryFrom<crate::convex_identity::AuthenticationToken> for AuthenticationToke
2323
AuthenticationToken::Admin(key, acting_as)
2424
},
2525
AuthenticationTokenProto::User(token) => AuthenticationToken::User(token),
26+
AuthenticationTokenProto::PlaintextUser(token) => {
27+
AuthenticationToken::PlaintextUser(token)
28+
},
2629
AuthenticationTokenProto::None(_) => AuthenticationToken::None,
2730
};
2831
Ok(token)
@@ -40,6 +43,9 @@ impl From<AuthenticationToken> for crate::convex_identity::AuthenticationToken {
4043
})
4144
},
4245
AuthenticationToken::User(token) => AuthenticationTokenProto::User(token),
46+
AuthenticationToken::PlaintextUser(token) => {
47+
AuthenticationTokenProto::PlaintextUser(token)
48+
},
4349
AuthenticationToken::None => AuthenticationTokenProto::None(()),
4450
};
4551
Self {

npm-packages/convex/src/browser/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type {
2323
SubscribeOptions,
2424
ConnectionState,
2525
AuthTokenFetcher,
26+
PlaintextAuthTokenFetcher,
2627
} from "./sync/client.js";
2728
export type { ConvexClientOptions } from "./simple_client.js";
2829
export { ConvexClient } from "./simple_client.js";

0 commit comments

Comments
 (0)