1use std::{
16 collections::HashMap,
17 env,
18 fmt::Debug,
19 ops::Add,
20 time::{Duration, SystemTime, UNIX_EPOCH},
21};
22
23use jsonwebtoken::{self, DecodingKey, EncodingKey, Header};
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26
27use crate::get_env_keys;
28
29pub const DEFAULT_TTL: Duration = Duration::from_secs(3600 * 6); #[derive(Debug, Error)]
32pub enum AccessTokenError {
33 #[error("Invalid API Key or Secret Key")]
34 InvalidKeys,
35 #[error("Invalid environment")]
36 InvalidEnv(#[from] env::VarError),
37 #[error("invalid claims: {0}")]
38 InvalidClaims(&'static str),
39 #[error("failed to encode jwt")]
40 Encoding(#[from] jsonwebtoken::errors::Error),
41}
42
43#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
44#[serde(default, rename_all = "camelCase")]
45pub struct VideoGrants {
46 #[serde(skip_serializing_if = "is_default")]
48 pub room_create: bool,
49 #[serde(skip_serializing_if = "is_default")]
50 pub room_list: bool,
51 #[serde(skip_serializing_if = "is_default")]
52 pub room_record: bool,
53
54 #[serde(skip_serializing_if = "is_default")]
56 pub room_admin: bool,
57 #[serde(skip_serializing_if = "is_default")]
58 pub room_join: bool,
59 #[serde(skip_serializing_if = "is_default")]
60 pub room: String,
61 #[serde(skip_serializing_if = "is_default")]
62 pub destination_room: String,
63
64 #[serde(skip_serializing_if = "is_default")]
66 pub can_publish: Option<bool>,
67 #[serde(skip_serializing_if = "is_default")]
68 pub can_subscribe: Option<bool>,
69 #[serde(skip_serializing_if = "is_default")]
70 pub can_publish_data: Option<bool>,
71
72 #[serde(skip_serializing_if = "is_default")]
75 pub can_publish_sources: Vec<String>, #[serde(skip_serializing_if = "is_default")]
79 pub can_update_own_metadata: Option<bool>,
80
81 #[serde(skip_serializing_if = "is_default")]
83 pub ingress_admin: bool, #[serde(skip_serializing_if = "is_default")]
87 pub hidden: bool,
88
89 #[serde(skip_serializing_if = "is_default")]
91 pub recorder: bool,
92
93 #[serde(skip_serializing_if = "is_default")]
95 pub agent: bool,
96}
97
98fn is_default<T: Default + PartialEq>(v: &T) -> bool {
99 *v == T::default()
100}
101
102impl VideoGrants {
103 pub fn can_publish(&self) -> bool {
104 self.can_publish.unwrap_or(true)
105 }
106
107 pub fn can_subscribe(&self) -> bool {
108 self.can_subscribe.unwrap_or(true)
109 }
110
111 pub fn can_publish_data(&self) -> bool {
112 self.can_publish_data.unwrap_or_else(|| self.can_publish())
113 }
114
115 pub fn can_update_own_metadata(&self) -> bool {
116 self.can_update_own_metadata.unwrap_or(false)
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121#[serde(rename_all = "camelCase")]
122pub struct SIPGrants {
123 #[serde(default, skip_serializing_if = "is_default")]
125 pub admin: bool,
126 #[serde(default, skip_serializing_if = "is_default")]
128 pub call: bool,
129}
130
131impl Default for SIPGrants {
132 fn default() -> Self {
133 Self { admin: false, call: false }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq)]
138#[serde(default)]
139#[serde(rename_all = "camelCase")]
140pub struct Claims {
141 pub exp: usize, pub iss: String, pub nbf: usize,
144 #[serde(skip_serializing_if = "is_default")]
145 pub sub: String, #[serde(skip_serializing_if = "is_default")]
148 pub name: String,
149 #[serde(skip_serializing_if = "is_default")]
150 pub kind: String,
151 #[serde(skip_serializing_if = "is_default")]
152 pub video: VideoGrants,
153 #[serde(skip_serializing_if = "is_default")]
154 pub sip: SIPGrants,
155 #[serde(skip_serializing_if = "is_default")]
156 pub sha256: String, #[serde(skip_serializing_if = "is_default")]
158 pub metadata: String,
159 #[serde(skip_serializing_if = "is_default")]
160 pub attributes: HashMap<String, String>,
161 #[serde(skip_serializing_if = "is_default")]
162 pub room_config: Option<livekit_protocol::RoomConfiguration>,
163}
164
165impl Claims {
166 pub fn from_unverified(token: &str) -> Result<Self, AccessTokenError> {
167 crate::jwt_provider::ensure_installed();
168 let token = jsonwebtoken::dangerous::insecure_decode::<Claims>(token)?;
169 Ok(token.claims)
170 }
171}
172
173#[derive(Clone)]
174pub struct AccessToken {
175 api_key: String,
176 api_secret: String,
177 claims: Claims,
178}
179
180impl Debug for AccessToken {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 f.debug_struct("AccessToken")
184 .field("api_key", &self.api_key)
185 .field("claims", &self.claims)
186 .finish()
187 }
188}
189
190impl AccessToken {
191 pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
192 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
193 Self {
194 api_key: api_key.to_owned(),
195 api_secret: api_secret.to_owned(),
196 claims: Claims {
197 exp: now.add(DEFAULT_TTL).as_secs() as usize,
198 iss: api_key.to_owned(),
199 nbf: now.as_secs() as usize,
200 sub: Default::default(),
201 name: Default::default(),
202 kind: Default::default(),
203 video: VideoGrants::default(),
204 sip: SIPGrants::default(),
205 sha256: Default::default(),
206 metadata: Default::default(),
207 attributes: HashMap::new(),
208 room_config: Default::default(),
209 },
210 }
211 }
212
213 #[cfg(test)]
214 pub fn from_parts(api_key: &str, api_secret: &str, claims: Claims) -> Self {
215 Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned(), claims }
216 }
217
218 pub fn new() -> Result<Self, AccessTokenError> {
219 let (api_key, api_secret) = get_env_keys()?;
221 Ok(Self::with_api_key(&api_key, &api_secret))
222 }
223
224 pub fn with_ttl(mut self, ttl: Duration) -> Self {
225 let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + ttl;
226 self.claims.exp = time.as_secs() as usize;
227 self
228 }
229
230 pub fn with_grants(mut self, grants: VideoGrants) -> Self {
231 self.claims.video = grants;
232 self
233 }
234
235 pub fn with_sip_grants(mut self, grants: SIPGrants) -> Self {
236 self.claims.sip = grants;
237 self
238 }
239
240 pub fn with_identity(mut self, identity: &str) -> Self {
241 self.claims.sub = identity.to_owned();
242 self
243 }
244
245 pub fn with_name(mut self, name: &str) -> Self {
246 self.claims.name = name.to_owned();
247 self
248 }
249
250 pub fn with_kind(mut self, kind: &str) -> Self {
251 self.claims.kind = kind.to_owned();
252 self
253 }
254
255 pub fn with_metadata(mut self, metadata: &str) -> Self {
256 self.claims.metadata = metadata.to_owned();
257 self
258 }
259
260 pub fn with_attributes<I, K, V>(mut self, attributes: I) -> Self
261 where
262 I: IntoIterator<Item = (K, V)>,
263 K: Into<String>,
264 V: Into<String>,
265 {
266 self.claims.attributes =
267 attributes.into_iter().map(|(k, v)| (k.into(), v.into())).collect::<HashMap<_, _>>();
268 self
269 }
270
271 pub fn with_sha256(mut self, sha256: &str) -> Self {
272 self.claims.sha256 = sha256.to_owned();
273 self
274 }
275
276 pub fn with_room_config(mut self, config: livekit_protocol::RoomConfiguration) -> Self {
277 self.claims.room_config = Some(config);
278 self
279 }
280
281 pub fn to_jwt(self) -> Result<String, AccessTokenError> {
282 crate::jwt_provider::ensure_installed();
283 if self.api_key.is_empty() || self.api_secret.is_empty() {
284 return Err(AccessTokenError::InvalidKeys);
285 }
286
287 if self.claims.video.room_join
288 && (self.claims.sub.is_empty() || self.claims.video.room.is_empty())
289 {
290 return Err(AccessTokenError::InvalidClaims(
291 "token grants room_join but doesn't have an identity or room",
292 ));
293 }
294
295 Ok(jsonwebtoken::encode(
296 &Header::new(jsonwebtoken::Algorithm::HS256),
297 &self.claims,
298 &EncodingKey::from_secret(self.api_secret.as_ref()),
299 )?)
300 }
301}
302
303#[derive(Clone)]
304pub struct TokenVerifier {
305 api_key: String,
306 api_secret: String,
307}
308
309impl Debug for TokenVerifier {
310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311 f.debug_struct("TokenVerifier").field("api_key", &self.api_key).finish()
312 }
313}
314
315impl TokenVerifier {
316 pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
317 Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned() }
318 }
319
320 pub fn new() -> Result<Self, AccessTokenError> {
321 let (api_key, api_secret) = get_env_keys()?;
322 Ok(Self::with_api_key(&api_key, &api_secret))
323 }
324
325 pub fn verify(&self, token: &str) -> Result<Claims, AccessTokenError> {
326 crate::jwt_provider::ensure_installed();
327 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
328 validation.validate_exp = true;
329 validation.validate_nbf = true;
330 validation.set_issuer(&[&self.api_key]);
331
332 let token = jsonwebtoken::decode::<Claims>(
333 token,
334 &DecodingKey::from_secret(self.api_secret.as_ref()),
335 &validation,
336 )?;
337
338 Ok(token.claims)
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use std::time::Duration;
345
346 use super::{AccessToken, Claims, SIPGrants, TokenVerifier, VideoGrants};
347
348 const TEST_API_KEY: &str = "myapikey";
349 const TEST_API_SECRET: &str = "thiskeyistotallyunsafe";
350 const TEST_TOKEN: &str = include_str!("test_token.txt");
351
352 #[test]
353 fn test_access_token() {
354 let room_config = livekit_protocol::RoomConfiguration {
355 name: "name".to_string(),
356 agents: vec![livekit_protocol::RoomAgentDispatch {
357 agent_name: "test-agent".to_string(),
358 metadata: "test-metadata".to_string(),
359 ..Default::default()
360 }],
361 ..Default::default()
362 };
363
364 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
365 .with_ttl(Duration::from_secs(60))
366 .with_identity("test")
367 .with_name("test")
368 .with_grants(VideoGrants::default())
369 .with_room_config(room_config.clone())
370 .to_jwt()
371 .unwrap();
372
373 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
374 let claims = verifier.verify(&token).unwrap();
375
376 assert_eq!(claims.sub, "test");
377 assert_eq!(claims.name, "test");
378 assert_eq!(claims.iss, TEST_API_KEY);
379 assert_eq!(claims.room_config, Some(room_config));
380
381 let incorrect_issuer = TokenVerifier::with_api_key("incorrect", TEST_API_SECRET);
382 assert!(incorrect_issuer.verify(&token).is_err());
383
384 let incorrect_token = TokenVerifier::with_api_key(TEST_API_KEY, "incorrect");
385 assert!(incorrect_token.verify(&token).is_err());
386 }
387
388 #[test]
389 fn test_verify_token_with_room_config() {
390 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
391 let claims = verifier.verify(TEST_TOKEN).expect("Failed to verify token.");
393
394 assert_eq!(
395 super::Claims {
396 sub: "identity".to_string(),
397 name: "name".to_string(),
398 room_config: Some(livekit_protocol::RoomConfiguration {
399 agents: vec![livekit_protocol::RoomAgentDispatch {
400 agent_name: "test-agent".to_string(),
401 metadata: "test-metadata".to_string(),
402 ..Default::default()
403 }],
404 ..Default::default()
405 }),
406 ..claims.clone()
407 },
408 claims
409 );
410 }
411
412 #[test]
413 fn test_unverified_token() {
414 let claims = Claims::from_unverified(TEST_TOKEN).expect("Failed to parse token");
415
416 assert_eq!(claims.sub, "identity");
417 assert_eq!(claims.name, "name");
418 assert_eq!(claims.iss, TEST_API_KEY);
419 assert_eq!(
420 claims.room_config,
421 Some(livekit_protocol::RoomConfiguration {
422 agents: vec![livekit_protocol::RoomAgentDispatch {
423 agent_name: "test-agent".to_string(),
424 metadata: "test-metadata".to_string(),
425 ..Default::default()
426 }],
427 ..Default::default()
428 })
429 );
430
431 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
432 .with_ttl(Duration::from_secs(60))
433 .with_identity("test")
434 .with_name("test")
435 .with_grants(VideoGrants {
436 room_join: true,
437 room: "test-room".to_string(),
438 ..Default::default()
439 })
440 .to_jwt()
441 .unwrap();
442
443 let claims = Claims::from_unverified(&token).expect("Failed to parse fresh token");
444 assert_eq!(claims.sub, "test");
445 assert_eq!(claims.name, "test");
446 assert_eq!(claims.video.room, "test-room");
447 assert!(claims.video.room_join);
448
449 let parts: Vec<&str> = token.split('.').collect();
450 let malformed_token = format!("{}.{}.wrongsignature", parts[0], parts[1]);
451
452 let claims = Claims::from_unverified(&malformed_token)
453 .expect("Failed to parse token with wrong signature");
454 assert_eq!(claims.sub, "test");
455 assert_eq!(claims.name, "test");
456 }
457
458 #[test]
459 fn test_agent_grant_and_kind() {
460 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
461 .with_ttl(Duration::from_secs(60))
462 .with_identity("agent-1")
463 .with_kind("agent")
464 .with_grants(VideoGrants {
465 room_join: true,
466 room: "test-room".to_string(),
467 agent: true,
468 ..Default::default()
469 })
470 .to_jwt()
471 .expect("Failed to create token");
472
473 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
474 let claims = verifier.verify(&token).expect("Failed to verify token.");
475 assert_eq!(claims.kind, "agent");
476 assert!(claims.video.agent);
477
478 let payload = |token: &str| {
479 let _ = Claims::from_unverified(token).expect("Failed to parse token");
480 jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(token)
481 .expect("Failed to decode token")
482 .claims
483 };
484 let bare = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
485 .with_ttl(Duration::from_secs(60))
486 .with_grants(VideoGrants::default())
487 .to_jwt()
488 .expect("Failed to create token");
489 let p = payload(&bare);
490 let mut keys: Vec<&str> = p.as_object().unwrap().keys().map(String::as_str).collect();
491 keys.sort_unstable();
492 assert_eq!(keys, ["exp", "iss", "nbf"], "{p}");
493
494 let agent = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
495 .with_ttl(Duration::from_secs(60))
496 .with_grants(VideoGrants {
497 agent: true,
498 can_publish: Some(false),
499 ..Default::default()
500 })
501 .to_jwt()
502 .expect("Failed to create token");
503 let p = payload(&agent);
504 assert_eq!(p["video"], serde_json::json!({"agent": true, "canPublish": false}), "{p}");
505 let claims = Claims::from_unverified(&agent).expect("Failed to parse token");
506 assert!(claims.video.agent && !claims.video.can_publish() && claims.video.can_subscribe());
507 assert!(!claims.video.can_publish_data(), "absent canPublishData follows canPublish");
508 }
509
510 #[test]
511 fn test_defaults_are_not_serialized() {
512 assert_eq!(serde_json::to_string(&VideoGrants::default()).unwrap(), "{}");
513 let parsed: VideoGrants = serde_json::from_str("{}").unwrap();
514 assert_eq!(parsed, VideoGrants::default());
515 assert_eq!(serde_json::to_string(&parsed).unwrap(), "{}");
516 let explicit = VideoGrants {
517 can_publish: Some(false),
518 can_publish_data: Some(true),
519 ..Default::default()
520 };
521 assert_eq!(
522 serde_json::to_string(&explicit).unwrap(),
523 r#"{"canPublish":false,"canPublishData":true}"#
524 );
525 assert!(explicit.can_publish_data());
526
527 assert_eq!(serde_json::to_string(&SIPGrants::default()).unwrap(), "{}");
528 let parsed: SIPGrants = serde_json::from_str("{}").unwrap();
529 assert_eq!(parsed, SIPGrants::default());
530 assert_eq!(serde_json::to_string(&parsed).unwrap(), "{}");
531
532 let claims = Claims { exp: 1, iss: "k".to_string(), nbf: 0, ..Default::default() };
533 let json = r#"{"exp":1,"iss":"k","nbf":0}"#;
534 assert_eq!(serde_json::to_string(&claims).unwrap(), json);
535 let parsed: Claims = serde_json::from_str(json).unwrap();
536 assert_eq!(parsed, claims);
537 assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
538 }
539}