Skip to content

Commit 3faab50

Browse files
pblazejladvoc
authored andcommitted
Parse unverified JWT (#756)
* Unverified token * CR: Names * CR: Test token
1 parent ba06af5 commit 3faab50

File tree

4 files changed

+97
-25
lines changed

4 files changed

+97
-25
lines changed

livekit-api/src/access_token.rs

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,24 @@ pub struct Claims {
151151
pub room_config: Option<livekit_protocol::RoomConfiguration>,
152152
}
153153

154+
impl Claims {
155+
pub fn from_unverified(token: &str) -> Result<Self, AccessTokenError> {
156+
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
157+
validation.validate_exp = true;
158+
validation.validate_nbf = true;
159+
validation.set_required_spec_claims::<String>(&[]);
160+
validation.insecure_disable_signature_validation();
161+
162+
let token = jsonwebtoken::decode::<Claims>(
163+
token,
164+
&DecodingKey::from_secret(&[]),
165+
&validation,
166+
)?;
167+
168+
Ok(token.claims)
169+
}
170+
}
171+
154172
#[derive(Clone)]
155173
pub struct AccessToken {
156174
api_key: String,
@@ -299,10 +317,6 @@ impl TokenVerifier {
299317
pub fn verify(&self, token: &str) -> Result<Claims, AccessTokenError> {
300318
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
301319
validation.validate_exp = true;
302-
#[cfg(test)] // FIXME: TEST_TOKEN is expired, TODO: generate TEST_TOKEN at test runtime
303-
{
304-
validation.validate_exp = false;
305-
}
306320
validation.validate_nbf = true;
307321
validation.set_issuer(&[&self.api_key]);
308322

@@ -320,7 +334,7 @@ impl TokenVerifier {
320334
mod tests {
321335
use std::time::Duration;
322336

323-
use super::{AccessToken, TokenVerifier, VideoGrants};
337+
use super::{AccessToken, Claims, TokenVerifier, VideoGrants};
324338

325339
const TEST_API_KEY: &str = "myapikey";
326340
const TEST_API_SECRET: &str = "thiskeyistotallyunsafe";
@@ -383,4 +397,44 @@ mod tests {
383397
claims
384398
);
385399
}
400+
401+
#[test]
402+
fn test_unverified_token() {
403+
let claims = Claims::from_unverified(TEST_TOKEN).expect("Failed to parse token");
404+
405+
assert_eq!(claims.sub, "identity");
406+
assert_eq!(claims.name, "name");
407+
assert_eq!(claims.iss, TEST_API_KEY);
408+
assert_eq!(
409+
claims.room_config,
410+
Some(livekit_protocol::RoomConfiguration {
411+
agents: vec![livekit_protocol::RoomAgentDispatch {
412+
agent_name: "test-agent".to_string(),
413+
metadata: "test-metadata".to_string(),
414+
}],
415+
..Default::default()
416+
})
417+
);
418+
419+
let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
420+
.with_ttl(Duration::from_secs(60))
421+
.with_identity("test")
422+
.with_name("test")
423+
.with_grants(VideoGrants { room_join: true, room: "test-room".to_string(), ..Default::default() })
424+
.to_jwt()
425+
.unwrap();
426+
427+
let claims = Claims::from_unverified(&token).expect("Failed to parse fresh token");
428+
assert_eq!(claims.sub, "test");
429+
assert_eq!(claims.name, "test");
430+
assert_eq!(claims.video.room, "test-room");
431+
assert!(claims.video.room_join);
432+
433+
let parts: Vec<&str> = token.split('.').collect();
434+
let malformed_token = format!("{}.{}.wrongsignature", parts[0], parts[1]);
435+
436+
let claims = Claims::from_unverified(&malformed_token).expect("Failed to parse token with wrong signature");
437+
assert_eq!(claims.sub, "test");
438+
assert_eq!(claims.name, "test");
439+
}
386440
}

livekit-api/src/test_token.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibmFtZSIsInZpZGVvIjp7InJvb21Kb2luIjp0cnVlLCJyb29tIjoibXktcm9vbSIsImNhblB1Ymxpc2giOnRydWUsImNhblN1YnNjcmliZSI6dHJ1ZSwiY2FuUHVibGlzaERhdGEiOnRydWV9LCJyb29tQ29uZmlnIjp7ImFnZW50cyI6W3siYWdlbnROYW1lIjoidGVzdC1hZ2VudCIsIm1ldGFkYXRhIjoidGVzdC1tZXRhZGF0YSJ9XX0sInN1YiI6ImlkZW50aXR5IiwiaXNzIjoibXlhcGlrZXkiLCJuYmYiOjE3NDE4Njk2NzksImV4cCI6MTc0MTg5MTI3OX0.9_eOcZ1RNaRXxwdU36LTYoxsHtv_IhfhLk-kTd8MDY4
1+
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibmFtZSIsInZpZGVvIjp7InJvb21Kb2luIjp0cnVlLCJyb29tIjoibXktcm9vbSIsImNhblB1Ymxpc2giOnRydWUsImNhblN1YnNjcmliZSI6dHJ1ZSwiY2FuUHVibGlzaERhdGEiOnRydWV9LCJyb29tQ29uZmlnIjp7ImFnZW50cyI6W3siYWdlbnROYW1lIjoidGVzdC1hZ2VudCIsIm1ldGFkYXRhIjoidGVzdC1tZXRhZGF0YSJ9XX0sInN1YiI6ImlkZW50aXR5IiwiaXNzIjoibXlhcGlrZXkiLCJuYmYiOjE3NDE4Njk2NzksImV4cCI6NDEwMjQ0NDgwMH0.hmKovgw6sDbQUFZxAQ9Xjb9-VUzeBw1A6URTXFuCLMU

livekit-uniffi/python_test/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ def main():
2929

3030
credentials = ApiCredentials(key="devkey", secret="secret")
3131

32-
jwt = generate_token(
32+
jwt = token_generate(
3333
options=TokenOptions(room_name="test", identity="some_participant"),
3434
credentials=credentials,
3535
)
3636
print(f"Generated JWT: {jwt}")
3737

38-
decoded_grants = verify_token(
38+
decoded_grants = token_verify(
3939
token=jwt,
4040
credentials=credentials,
4141
)

livekit-uniffi/src/access_token.rs

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414

1515
use livekit_api::access_token::{
16-
AccessToken, AccessTokenError, SIPGrants, TokenVerifier, VideoGrants,
16+
self, AccessToken, AccessTokenError, SIPGrants, TokenVerifier, VideoGrants,
1717
};
1818
use std::{collections::HashMap, time::Duration};
1919

@@ -79,6 +79,25 @@ pub struct Claims {
7979
pub room_name: String,
8080
}
8181

82+
impl From<livekit_api::access_token::Claims> for Claims {
83+
fn from(claims: livekit_api::access_token::Claims) -> Self {
84+
let room_name = claims.room_config.map_or(String::default(), |config| config.name);
85+
Self {
86+
exp: claims.exp as u64,
87+
iss: claims.iss,
88+
nbf: claims.nbf as u64,
89+
sub: claims.sub,
90+
name: claims.name,
91+
video: claims.video,
92+
sip: claims.sip,
93+
sha256: claims.sha256,
94+
metadata: claims.metadata,
95+
attributes: claims.attributes,
96+
room_name,
97+
}
98+
}
99+
}
100+
82101
/// API credentials for access token generation and verification.
83102
#[derive(uniffi::Record)]
84103
pub struct ApiCredentials {
@@ -121,7 +140,7 @@ pub struct TokenOptions {
121140
/// variables `LIVEKIT_API_KEY` and `LIVEKIT_SECRET` respectively.
122141
///
123142
#[uniffi::export]
124-
pub fn generate_token(
143+
pub fn token_generate(
125144
options: TokenOptions,
126145
credentials: Option<ApiCredentials>,
127146
) -> Result<String, AccessTokenError> {
@@ -168,7 +187,7 @@ pub fn generate_token(
168187
/// variables `LIVEKIT_API_KEY` and `LIVEKIT_SECRET` respectively.
169188
///
170189
#[uniffi::export]
171-
pub fn verify_token(
190+
pub fn token_verify(
172191
token: &str,
173192
credentials: Option<ApiCredentials>,
174193
) -> Result<Claims, AccessTokenError> {
@@ -179,18 +198,17 @@ pub fn verify_token(
179198
None => TokenVerifier::new()?,
180199
};
181200
let claims = verifier.verify(token)?;
182-
let room_name = claims.room_config.map_or(String::default(), |config| config.name);
183-
Ok(Claims {
184-
exp: claims.exp as u64,
185-
iss: claims.iss,
186-
nbf: claims.nbf as u64,
187-
sub: claims.sub,
188-
name: claims.name,
189-
video: claims.video,
190-
sip: claims.sip,
191-
sha256: claims.sha256,
192-
metadata: claims.metadata,
193-
attributes: claims.attributes,
194-
room_name,
195-
})
201+
Ok(claims.into())
202+
}
203+
204+
/// Parses an access token without verifying its signature.
205+
///
206+
/// This is useful when you want to inspect token contents without having the secret.
207+
/// The token's expiration (exp) and not-before (nbf) times are still validated.
208+
/// WARNING: Do not use this for authentication - the signature is not verified!
209+
///
210+
#[uniffi::export]
211+
pub fn token_claims_from_unverified(token: &str) -> Result<Claims, AccessTokenError> {
212+
let claims = access_token::Claims::from_unverified(token)?;
213+
Ok(claims.into())
196214
}

0 commit comments

Comments
 (0)