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, Default, Serialize, Deserialize, PartialEq)]
138#[serde(default, rename_all = "camelCase")]
139pub struct AgentGrants {
140 #[serde(skip_serializing_if = "is_default")]
142 pub admin: bool,
143 #[serde(skip_serializing_if = "is_default")]
145 pub simulation_admin: bool,
146 #[serde(skip_serializing_if = "is_default")]
148 pub database_admin: bool,
149 #[serde(skip_serializing_if = "is_default")]
151 pub dispatch_admin: bool,
152}
153
154#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
155#[serde(default, rename_all = "camelCase")]
156pub struct AgentEndpointGrants {
157 #[serde(skip_serializing_if = "is_default")]
159 pub call: bool,
160 #[serde(skip_serializing_if = "is_default")]
162 pub agent_name: String,
163 #[serde(skip_serializing_if = "is_default")]
165 pub deployment: String,
166}
167
168impl AgentEndpointGrants {
169 pub fn allows(&self, agent_name: &str, deployment: &str) -> bool {
170 self.call
171 && (self.agent_name.is_empty() || self.agent_name == agent_name)
172 && (self.deployment.is_empty() || self.deployment == deployment)
173 }
174}
175
176#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
177#[serde(default, rename_all = "camelCase")]
178pub struct AgentThreadGrants {
179 #[serde(skip_serializing_if = "is_default")]
180 pub all: bool,
181 #[serde(skip_serializing_if = "is_default")]
182 pub scope: String,
183 #[serde(skip_serializing_if = "is_default")]
184 pub thread_ids: Vec<String>,
185 #[serde(skip_serializing_if = "is_default")]
186 pub list: bool,
187 #[serde(skip_serializing_if = "is_default")]
188 pub create: bool,
189 #[serde(skip_serializing_if = "is_default")]
190 pub write: bool,
191 #[serde(skip_serializing_if = "is_default")]
192 pub read: bool,
193 #[serde(skip_serializing_if = "is_default")]
194 pub delete: bool,
195}
196
197impl AgentThreadGrants {
198 pub fn can_create(&self, scope: &str) -> bool {
199 self.create && self.in_scope(scope)
200 }
201
202 pub fn can_list(&self, scope: &str) -> bool {
203 self.list && self.in_scope(scope)
204 }
205
206 pub fn can_update(&self) -> bool {
207 self.all && self.write
208 }
209
210 pub fn can_read(&self, thread_id: &str) -> bool {
211 self.read && self.includes(thread_id)
212 }
213
214 pub fn can_write(&self, thread_id: &str) -> bool {
215 self.write && self.includes(thread_id)
216 }
217
218 pub fn can_delete(&self, thread_id: &str) -> bool {
219 self.delete && self.includes(thread_id)
220 }
221
222 fn in_scope(&self, scope: &str) -> bool {
223 self.all || (!self.scope.is_empty() && self.scope == scope)
224 }
225
226 fn includes(&self, thread_id: &str) -> bool {
227 !thread_id.is_empty() && (self.all || self.thread_ids.iter().any(|id| id == thread_id))
228 }
229}
230
231#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq)]
232#[serde(default)]
233#[serde(rename_all = "camelCase")]
234pub struct Claims {
235 pub exp: usize, pub iss: String, pub nbf: usize,
238 #[serde(skip_serializing_if = "is_default")]
239 pub sub: String, #[serde(skip_serializing_if = "is_default")]
242 pub name: String,
243 #[serde(skip_serializing_if = "is_default")]
244 pub kind: String,
245 #[serde(skip_serializing_if = "is_default")]
246 pub video: VideoGrants,
247 #[serde(skip_serializing_if = "is_default")]
248 pub sip: SIPGrants,
249 #[serde(skip_serializing_if = "is_default")]
250 pub agent: AgentGrants,
251 #[serde(skip_serializing_if = "is_default")]
252 pub agent_endpoint: AgentEndpointGrants,
253 #[serde(skip_serializing_if = "is_default")]
254 pub agent_thread: AgentThreadGrants,
255 #[serde(skip_serializing_if = "is_default")]
256 pub sha256: String, #[serde(skip_serializing_if = "is_default")]
258 pub metadata: String,
259 #[serde(skip_serializing_if = "is_default")]
260 pub attributes: HashMap<String, String>,
261 #[serde(skip_serializing_if = "is_default")]
262 pub room_config: Option<livekit_protocol::RoomConfiguration>,
263}
264
265impl Claims {
266 pub fn from_unverified(token: &str) -> Result<Self, AccessTokenError> {
267 crate::jwt_provider::ensure_installed();
268 let token = jsonwebtoken::dangerous::insecure_decode::<Claims>(token)?;
269 Ok(token.claims)
270 }
271}
272
273#[derive(Clone)]
274pub struct AccessToken {
275 api_key: String,
276 api_secret: String,
277 claims: Claims,
278}
279
280impl Debug for AccessToken {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 f.debug_struct("AccessToken")
284 .field("api_key", &self.api_key)
285 .field("claims", &self.claims)
286 .finish()
287 }
288}
289
290impl AccessToken {
291 pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
292 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
293 Self {
294 api_key: api_key.to_owned(),
295 api_secret: api_secret.to_owned(),
296 claims: Claims {
297 exp: now.add(DEFAULT_TTL).as_secs() as usize,
298 iss: api_key.to_owned(),
299 nbf: now.as_secs() as usize,
300 sub: Default::default(),
301 name: Default::default(),
302 kind: Default::default(),
303 video: VideoGrants::default(),
304 sip: SIPGrants::default(),
305 agent: AgentGrants::default(),
306 agent_endpoint: AgentEndpointGrants::default(),
307 agent_thread: AgentThreadGrants::default(),
308 sha256: Default::default(),
309 metadata: Default::default(),
310 attributes: HashMap::new(),
311 room_config: Default::default(),
312 },
313 }
314 }
315
316 #[cfg(test)]
317 pub fn from_parts(api_key: &str, api_secret: &str, claims: Claims) -> Self {
318 Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned(), claims }
319 }
320
321 pub fn new() -> Result<Self, AccessTokenError> {
322 let (api_key, api_secret) = get_env_keys()?;
324 Ok(Self::with_api_key(&api_key, &api_secret))
325 }
326
327 pub fn with_ttl(mut self, ttl: Duration) -> Self {
328 let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + ttl;
329 self.claims.exp = time.as_secs() as usize;
330 self
331 }
332
333 pub fn with_grants(mut self, grants: VideoGrants) -> Self {
334 self.claims.video = grants;
335 self
336 }
337
338 pub fn with_sip_grants(mut self, grants: SIPGrants) -> Self {
339 self.claims.sip = grants;
340 self
341 }
342
343 pub fn with_agent_grants(mut self, grants: AgentGrants) -> Self {
344 self.claims.agent = grants;
345 self
346 }
347
348 pub fn with_agent_endpoint_grants(mut self, grants: AgentEndpointGrants) -> Self {
349 self.claims.agent_endpoint = grants;
350 self
351 }
352
353 pub fn with_agent_thread_grants(mut self, grants: AgentThreadGrants) -> Self {
354 self.claims.agent_thread = grants;
355 self
356 }
357
358 pub fn with_identity(mut self, identity: &str) -> Self {
359 self.claims.sub = identity.to_owned();
360 self
361 }
362
363 pub fn with_name(mut self, name: &str) -> Self {
364 self.claims.name = name.to_owned();
365 self
366 }
367
368 pub fn with_kind(mut self, kind: &str) -> Self {
369 self.claims.kind = kind.to_owned();
370 self
371 }
372
373 pub fn with_metadata(mut self, metadata: &str) -> Self {
374 self.claims.metadata = metadata.to_owned();
375 self
376 }
377
378 pub fn with_attributes<I, K, V>(mut self, attributes: I) -> Self
379 where
380 I: IntoIterator<Item = (K, V)>,
381 K: Into<String>,
382 V: Into<String>,
383 {
384 self.claims.attributes =
385 attributes.into_iter().map(|(k, v)| (k.into(), v.into())).collect::<HashMap<_, _>>();
386 self
387 }
388
389 pub fn with_sha256(mut self, sha256: &str) -> Self {
390 self.claims.sha256 = sha256.to_owned();
391 self
392 }
393
394 pub fn with_room_config(mut self, config: livekit_protocol::RoomConfiguration) -> Self {
395 self.claims.room_config = Some(config);
396 self
397 }
398
399 pub fn to_jwt(self) -> Result<String, AccessTokenError> {
400 crate::jwt_provider::ensure_installed();
401 if self.api_key.is_empty() || self.api_secret.is_empty() {
402 return Err(AccessTokenError::InvalidKeys);
403 }
404
405 if self.claims.video.room_join
406 && (self.claims.sub.is_empty() || self.claims.video.room.is_empty())
407 {
408 return Err(AccessTokenError::InvalidClaims(
409 "token grants room_join but doesn't have an identity or room",
410 ));
411 }
412
413 Ok(jsonwebtoken::encode(
414 &Header::new(jsonwebtoken::Algorithm::HS256),
415 &self.claims,
416 &EncodingKey::from_secret(self.api_secret.as_ref()),
417 )?)
418 }
419}
420
421#[derive(Clone)]
422pub struct TokenVerifier {
423 api_key: String,
424 api_secret: String,
425}
426
427impl Debug for TokenVerifier {
428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 f.debug_struct("TokenVerifier").field("api_key", &self.api_key).finish()
430 }
431}
432
433impl TokenVerifier {
434 pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
435 Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned() }
436 }
437
438 pub fn new() -> Result<Self, AccessTokenError> {
439 let (api_key, api_secret) = get_env_keys()?;
440 Ok(Self::with_api_key(&api_key, &api_secret))
441 }
442
443 pub fn verify(&self, token: &str) -> Result<Claims, AccessTokenError> {
444 crate::jwt_provider::ensure_installed();
445 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
446 validation.validate_exp = true;
447 validation.validate_nbf = true;
448 validation.set_issuer(&[&self.api_key]);
449
450 let token = jsonwebtoken::decode::<Claims>(
451 token,
452 &DecodingKey::from_secret(self.api_secret.as_ref()),
453 &validation,
454 )?;
455
456 Ok(token.claims)
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use std::time::Duration;
463
464 use super::{
465 AccessToken, AgentEndpointGrants, AgentGrants, AgentThreadGrants, Claims, SIPGrants,
466 TokenVerifier, VideoGrants,
467 };
468
469 const TEST_API_KEY: &str = "myapikey";
470 const TEST_API_SECRET: &str = "thiskeyistotallyunsafe";
471 const TEST_TOKEN: &str = include_str!("test_token.txt");
472
473 #[test]
474 fn test_access_token() {
475 let room_config = livekit_protocol::RoomConfiguration {
476 name: "name".to_string(),
477 agents: vec![livekit_protocol::RoomAgentDispatch {
478 agent_name: "test-agent".to_string(),
479 metadata: "test-metadata".to_string(),
480 ..Default::default()
481 }],
482 ..Default::default()
483 };
484
485 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
486 .with_ttl(Duration::from_secs(60))
487 .with_identity("test")
488 .with_name("test")
489 .with_grants(VideoGrants::default())
490 .with_room_config(room_config.clone())
491 .to_jwt()
492 .unwrap();
493
494 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
495 let claims = verifier.verify(&token).unwrap();
496
497 assert_eq!(claims.sub, "test");
498 assert_eq!(claims.name, "test");
499 assert_eq!(claims.iss, TEST_API_KEY);
500 assert_eq!(claims.room_config, Some(room_config));
501
502 let incorrect_issuer = TokenVerifier::with_api_key("incorrect", TEST_API_SECRET);
503 assert!(incorrect_issuer.verify(&token).is_err());
504
505 let incorrect_token = TokenVerifier::with_api_key(TEST_API_KEY, "incorrect");
506 assert!(incorrect_token.verify(&token).is_err());
507 }
508
509 #[test]
510 fn test_verify_token_with_room_config() {
511 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
512 let claims = verifier.verify(TEST_TOKEN).expect("Failed to verify token.");
514
515 assert_eq!(
516 super::Claims {
517 sub: "identity".to_string(),
518 name: "name".to_string(),
519 room_config: Some(livekit_protocol::RoomConfiguration {
520 agents: vec![livekit_protocol::RoomAgentDispatch {
521 agent_name: "test-agent".to_string(),
522 metadata: "test-metadata".to_string(),
523 ..Default::default()
524 }],
525 ..Default::default()
526 }),
527 ..claims.clone()
528 },
529 claims
530 );
531 }
532
533 #[test]
534 fn test_unverified_token() {
535 let claims = Claims::from_unverified(TEST_TOKEN).expect("Failed to parse token");
536
537 assert_eq!(claims.sub, "identity");
538 assert_eq!(claims.name, "name");
539 assert_eq!(claims.iss, TEST_API_KEY);
540 assert_eq!(
541 claims.room_config,
542 Some(livekit_protocol::RoomConfiguration {
543 agents: vec![livekit_protocol::RoomAgentDispatch {
544 agent_name: "test-agent".to_string(),
545 metadata: "test-metadata".to_string(),
546 ..Default::default()
547 }],
548 ..Default::default()
549 })
550 );
551
552 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
553 .with_ttl(Duration::from_secs(60))
554 .with_identity("test")
555 .with_name("test")
556 .with_grants(VideoGrants {
557 room_join: true,
558 room: "test-room".to_string(),
559 ..Default::default()
560 })
561 .to_jwt()
562 .unwrap();
563
564 let claims = Claims::from_unverified(&token).expect("Failed to parse fresh token");
565 assert_eq!(claims.sub, "test");
566 assert_eq!(claims.name, "test");
567 assert_eq!(claims.video.room, "test-room");
568 assert!(claims.video.room_join);
569
570 let parts: Vec<&str> = token.split('.').collect();
571 let malformed_token = format!("{}.{}.wrongsignature", parts[0], parts[1]);
572
573 let claims = Claims::from_unverified(&malformed_token)
574 .expect("Failed to parse token with wrong signature");
575 assert_eq!(claims.sub, "test");
576 assert_eq!(claims.name, "test");
577 }
578
579 #[test]
580 fn test_agent_grant_and_kind() {
581 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
582 .with_ttl(Duration::from_secs(60))
583 .with_identity("agent-1")
584 .with_kind("agent")
585 .with_grants(VideoGrants {
586 room_join: true,
587 room: "test-room".to_string(),
588 agent: true,
589 ..Default::default()
590 })
591 .to_jwt()
592 .expect("Failed to create token");
593
594 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
595 let claims = verifier.verify(&token).expect("Failed to verify token.");
596 assert_eq!(claims.kind, "agent");
597 assert!(claims.video.agent);
598
599 let payload = |token: &str| {
600 let _ = Claims::from_unverified(token).expect("Failed to parse token");
601 jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(token)
602 .expect("Failed to decode token")
603 .claims
604 };
605 let bare = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
606 .with_ttl(Duration::from_secs(60))
607 .with_grants(VideoGrants::default())
608 .to_jwt()
609 .expect("Failed to create token");
610 let p = payload(&bare);
611 let mut keys: Vec<&str> = p.as_object().unwrap().keys().map(String::as_str).collect();
612 keys.sort_unstable();
613 assert_eq!(keys, ["exp", "iss", "nbf"], "{p}");
614
615 let agent = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
616 .with_ttl(Duration::from_secs(60))
617 .with_grants(VideoGrants {
618 agent: true,
619 can_publish: Some(false),
620 ..Default::default()
621 })
622 .to_jwt()
623 .expect("Failed to create token");
624 let p = payload(&agent);
625 assert_eq!(p["video"], serde_json::json!({"agent": true, "canPublish": false}), "{p}");
626 let claims = Claims::from_unverified(&agent).expect("Failed to parse token");
627 assert!(claims.video.agent && !claims.video.can_publish() && claims.video.can_subscribe());
628 assert!(!claims.video.can_publish_data(), "absent canPublishData follows canPublish");
629 }
630
631 #[test]
632 fn test_defaults_are_not_serialized() {
633 assert_eq!(serde_json::to_string(&VideoGrants::default()).unwrap(), "{}");
634 let parsed: VideoGrants = serde_json::from_str("{}").unwrap();
635 assert_eq!(parsed, VideoGrants::default());
636 assert_eq!(serde_json::to_string(&parsed).unwrap(), "{}");
637 let explicit = VideoGrants {
638 can_publish: Some(false),
639 can_publish_data: Some(true),
640 ..Default::default()
641 };
642 assert_eq!(
643 serde_json::to_string(&explicit).unwrap(),
644 r#"{"canPublish":false,"canPublishData":true}"#
645 );
646 assert!(explicit.can_publish_data());
647
648 assert_eq!(serde_json::to_string(&SIPGrants::default()).unwrap(), "{}");
649 let parsed: SIPGrants = serde_json::from_str("{}").unwrap();
650 assert_eq!(parsed, SIPGrants::default());
651 assert_eq!(serde_json::to_string(&parsed).unwrap(), "{}");
652
653 let claims = Claims { exp: 1, iss: "k".to_string(), nbf: 0, ..Default::default() };
654 let json = r#"{"exp":1,"iss":"k","nbf":0}"#;
655 assert_eq!(serde_json::to_string(&claims).unwrap(), json);
656 let parsed: Claims = serde_json::from_str(json).unwrap();
657 assert_eq!(parsed, claims);
658 assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
659 }
660
661 #[test]
662 fn test_agent_grants_claim_names() {
663 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
664 .with_agent_grants(AgentGrants { database_admin: true, ..Default::default() })
665 .with_agent_endpoint_grants(AgentEndpointGrants {
666 call: true,
667 agent_name: "a".into(),
668 ..Default::default()
669 })
670 .with_agent_thread_grants(AgentThreadGrants {
671 scope: "s".into(),
672 thread_ids: vec!["t".into()],
673 list: true,
674 read: true,
675 ..Default::default()
676 })
677 .to_jwt()
678 .unwrap();
679 let p =
680 jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(&token).unwrap().claims;
681 assert_eq!(p["agent"], serde_json::json!({"databaseAdmin": true}));
682 assert_eq!(p["agentEndpoint"], serde_json::json!({"call": true, "agentName": "a"}));
683 assert_eq!(
684 p["agentThread"],
685 serde_json::json!({"scope": "s", "threadIds": ["t"], "list": true, "read": true})
686 );
687 let claims = Claims::from_unverified(&token).unwrap();
688 assert!(claims.agent_thread.can_list("s") && claims.agent_thread.can_read("t"));
689 assert!(!claims.agent_thread.can_read("u") && !claims.agent_thread.can_create("s"));
690 assert!(claims.agent_endpoint.allows("a", "default"));
691 }
692}