Skip to content

Commit b13f12d

Browse files
JulienLavocatjsdt
andauthored
Add extra claims to v1/identity/websocket-token (#3705)
# Description of Changes Due to a limitation around passing headers to a WebSocket connection, The typescript SDK rely on the endpoint `/v1/identity/websocket-token` to get a new, short-lived token. Currently, this endpoint strips all the other claims from the token and only returns the following claims: - `hex_identity` - `sub` - `iss` - `aud` - `iat` - `exp` This PR aims to fix this issue by introducing a new member field `extra` to `SpacetimeIdentityClaims` and `TokenClaims` and letting serde do its job. # API and ABI breaking changes None # Expected complexity level and risk 2 - The change is trivial (1) but I'm not 100% familiar with all the places where we would be signing a token (1). # Testing 1. `curl` the endpoint and checking that the token returned contains all the expected claims 2. Check that that the endpoint `v1/identity` still correctly issues and identity and token --------- Co-authored-by: Jeffrey Dallatezza <jeffreydallatezza@gmail.com>
1 parent e818ef2 commit b13f12d

File tree

4 files changed

+53
-3
lines changed

4 files changed

+53
-3
lines changed

crates/auth/src/identity.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub use jsonwebtoken::{DecodingKey, EncodingKey};
44
use serde::Deserializer;
55
use serde::{Deserialize, Serialize};
66
use spacetimedb_lib::Identity;
7+
use std::collections::HashMap;
78
use std::time::SystemTime;
89

910
#[derive(Debug, Clone)]
@@ -41,6 +42,9 @@ pub struct SpacetimeIdentityClaims {
4142
pub iat: SystemTime,
4243
#[serde_as(as = "Option<serde_with::TimestampSeconds>")]
4344
pub exp: Option<SystemTime>,
45+
46+
#[serde(flatten)]
47+
pub extra: Option<HashMap<String, serde_json::Value>>,
4448
}
4549

4650
fn deserialize_audience<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
@@ -84,6 +88,10 @@ pub struct IncomingClaims {
8488
pub iat: SystemTime,
8589
#[serde_as(as = "Option<serde_with::TimestampSeconds>")]
8690
pub exp: Option<SystemTime>,
91+
92+
/// All remaining claims from the JWT payload
93+
#[serde(flatten)]
94+
pub extra: Option<HashMap<String, serde_json::Value>>,
8795
}
8896

8997
impl TryInto<SpacetimeIdentityClaims> for IncomingClaims {
@@ -122,6 +130,7 @@ impl TryInto<SpacetimeIdentityClaims> for IncomingClaims {
122130
audience: self.audience,
123131
iat: self.iat,
124132
exp: self.exp,
133+
extra: self.extra,
125134
})
126135
}
127136
}

crates/client-api/src/auth.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use spacetimedb::auth::token_validation::{
1414
use spacetimedb::auth::JwtKeys;
1515
use spacetimedb::energy::EnergyQuanta;
1616
use spacetimedb::identity::Identity;
17+
use std::collections::HashMap;
1718
use std::time::{Duration, SystemTime};
1819
use uuid::Uuid;
1920

@@ -117,15 +118,16 @@ pub struct TokenClaims {
117118
pub issuer: String,
118119
pub subject: String,
119120
pub audience: Vec<String>,
121+
pub extra: Option<HashMap<String, serde_json::Value>>,
120122
}
121123

122124
impl From<SpacetimeAuth> for TokenClaims {
123125
fn from(auth: SpacetimeAuth) -> Self {
124126
Self {
125127
issuer: auth.claims.issuer,
126128
subject: auth.claims.subject,
127-
// This will need to be changed when we care about audiencies.
128-
audience: Vec::new(),
129+
audience: auth.claims.audience,
130+
extra: auth.claims.extra,
129131
}
130132
}
131133
}
@@ -136,6 +138,7 @@ impl TokenClaims {
136138
issuer,
137139
subject,
138140
audience: Vec::new(),
141+
extra: None,
139142
}
140143
}
141144

@@ -159,6 +162,7 @@ impl TokenClaims {
159162
subject: self.subject.clone(),
160163
issuer: self.issuer.clone(),
161164
audience: self.audience.clone(),
165+
extra: self.extra.clone(),
162166
iat,
163167
exp,
164168
};
@@ -184,6 +188,7 @@ impl SpacetimeAuth {
184188
subject: subject.clone(),
185189
// Placeholder audience.
186190
audience: vec!["spacetimedb".to_string()],
191+
extra: None,
187192
};
188193

189194
let (claims, token) = claims.encode_and_sign(ctx.jwt_auth_provider()).map_err(log_and_500)?;
@@ -280,7 +285,7 @@ mod tests {
280285
use anyhow::Ok;
281286

282287
use spacetimedb::auth::{token_validation::TokenValidator, JwtKeys};
283-
use std::collections::HashSet;
288+
use std::collections::{HashMap, HashSet};
284289

285290
// Make sure that when we encode TokenClaims, we can decode to get the expected identity.
286291
#[tokio::test]
@@ -291,12 +296,42 @@ mod tests {
291296
issuer: "localhost".to_string(),
292297
subject: "test-subject".to_string(),
293298
audience: vec!["spacetimedb".to_string()],
299+
extra: None,
300+
};
301+
let id = claims.id();
302+
let (_, token) = claims.encode_and_sign(&kp.private)?;
303+
let decoded = kp.public.validate_token(&token).await?;
304+
305+
assert_eq!(decoded.identity, id);
306+
Ok(())
307+
}
308+
309+
fn to_hashmap(value: serde_json::Value) -> HashMap<String, serde_json::Value> {
310+
let mut map = HashMap::new();
311+
value.as_object().unwrap().iter().for_each(|(k, v)| {
312+
map.insert(k.clone(), v.clone());
313+
});
314+
map
315+
}
316+
317+
// Make sure that when we encode TokenClaims, we can decode the extra claims.
318+
#[tokio::test]
319+
async fn decode_encoded_token_with_extra_claims() -> Result<(), anyhow::Error> {
320+
let kp = JwtKeys::generate()?;
321+
322+
let claims = TokenClaims {
323+
issuer: "localhost".to_string(),
324+
subject: "test-subject".to_string(),
325+
audience: vec!["spacetimedb".to_string()],
326+
extra: Some(to_hashmap(serde_json::json!({"custom_claim": "value"}))),
294327
};
295328
let id = claims.id();
296329
let (_, token) = claims.encode_and_sign(&kp.private)?;
297330
let decoded = kp.public.validate_token(&token).await?;
298331

299332
assert_eq!(decoded.identity, id);
333+
let custom_claim_value = decoded.extra.as_ref().unwrap().get("custom_claim").unwrap();
334+
assert_eq!(custom_claim_value.as_str().unwrap(), "value");
300335
Ok(())
301336
}
302337

@@ -310,6 +345,7 @@ mod tests {
310345
issuer: "localhost".to_string(),
311346
subject: "test-subject".to_string(),
312347
audience: vec![dummy_audience.clone()],
348+
extra: None,
313349
};
314350
let (_, token) = claims.encode_and_sign(&kp.private)?;
315351
let st_creds = SpacetimeCreds::from_signed_token(token);

crates/core/src/auth/token_validation.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ mod tests {
349349
audience: vec![],
350350
iat: std::time::SystemTime::now(),
351351
exp: None,
352+
extra: None,
352353
};
353354
let token = kp.private.sign(&orig_claims)?;
354355

@@ -391,6 +392,7 @@ mod tests {
391392
audience: vec![],
392393
iat: std::time::SystemTime::now(),
393394
exp: None,
395+
extra: None,
394396
};
395397
let token = kp.private.sign(&orig_claims)?;
396398

@@ -444,6 +446,7 @@ mod tests {
444446
audience: vec![],
445447
iat: std::time::SystemTime::now(),
446448
exp: None,
449+
extra: None,
447450
};
448451
let token = kp.private.sign(&orig_claims)?;
449452

@@ -609,6 +612,7 @@ mod tests {
609612
audience: vec![],
610613
iat: std::time::SystemTime::now(),
611614
exp: None,
615+
extra: None,
612616
};
613617
for kp in [kp1, kp2] {
614618
log::debug!("Testing with key {:?}", kp.kid);

crates/core/src/client/client_connection.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ impl ClientConnectionSender {
335335
audience: vec![],
336336
iat: SystemTime::now(),
337337
exp: None,
338+
extra: None,
338339
};
339340
let sender = Self {
340341
id,

0 commit comments

Comments
 (0)