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, Serialize, Deserialize, PartialEq)]
44#[serde(rename_all = "camelCase")]
45pub struct VideoGrants {
46 #[serde(default)]
48 pub room_create: bool,
49 #[serde(default)]
50 pub room_list: bool,
51 #[serde(default)]
52 pub room_record: bool,
53
54 #[serde(default)]
56 pub room_admin: bool,
57 #[serde(default)]
58 pub room_join: bool,
59 #[serde(default)]
60 pub room: String,
61 #[serde(default)]
62 pub destination_room: String,
63
64 #[serde(default = "default_true")]
66 pub can_publish: bool,
67 #[serde(default = "default_true")]
68 pub can_subscribe: bool,
69 #[serde(default = "default_true")]
70 pub can_publish_data: bool,
71
72 #[serde(default)]
75 pub can_publish_sources: Vec<String>, #[serde(default)]
79 pub can_update_own_metadata: bool,
80
81 #[serde(default)]
83 pub ingress_admin: bool, #[serde(default)]
87 pub hidden: bool,
88
89 #[serde(default)]
91 pub recorder: bool,
92}
93
94fn default_true() -> bool {
96 true
97}
98
99impl Default for VideoGrants {
100 fn default() -> Self {
101 Self {
102 room_create: false,
103 room_list: false,
104 room_record: false,
105 room_admin: false,
106 room_join: false,
107 room: "".to_string(),
108 destination_room: "".to_string(),
109 can_publish: true,
110 can_subscribe: true,
111 can_publish_data: true,
112 can_publish_sources: Vec::default(),
113 can_update_own_metadata: false,
114 ingress_admin: false,
115 hidden: false,
116 recorder: false,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122#[serde(rename_all = "camelCase")]
123pub struct SIPGrants {
124 pub admin: bool,
126 pub call: bool,
128}
129
130impl Default for SIPGrants {
131 fn default() -> Self {
132 Self { admin: false, call: false }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq)]
137#[serde(default)]
138#[serde(rename_all = "camelCase")]
139pub struct Claims {
140 pub exp: usize, pub iss: String, pub nbf: usize,
143 pub sub: String, pub name: String,
146 pub video: VideoGrants,
147 pub sip: SIPGrants,
148 pub sha256: String, pub metadata: String,
150 pub attributes: HashMap<String, String>,
151 pub room_config: Option<livekit_protocol::RoomConfiguration>,
152}
153
154impl 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 =
163 jsonwebtoken::decode::<Claims>(token, &DecodingKey::from_secret(&[]), &validation)?;
164
165 Ok(token.claims)
166 }
167}
168
169#[derive(Clone)]
170pub struct AccessToken {
171 api_key: String,
172 api_secret: String,
173 claims: Claims,
174}
175
176impl Debug for AccessToken {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 f.debug_struct("AccessToken")
180 .field("api_key", &self.api_key)
181 .field("claims", &self.claims)
182 .finish()
183 }
184}
185
186impl AccessToken {
187 pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
188 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
189 Self {
190 api_key: api_key.to_owned(),
191 api_secret: api_secret.to_owned(),
192 claims: Claims {
193 exp: now.add(DEFAULT_TTL).as_secs() as usize,
194 iss: api_key.to_owned(),
195 nbf: now.as_secs() as usize,
196 sub: Default::default(),
197 name: Default::default(),
198 video: VideoGrants::default(),
199 sip: SIPGrants::default(),
200 sha256: Default::default(),
201 metadata: Default::default(),
202 attributes: HashMap::new(),
203 room_config: Default::default(),
204 },
205 }
206 }
207
208 #[cfg(test)]
209 pub fn from_parts(api_key: &str, api_secret: &str, claims: Claims) -> Self {
210 Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned(), claims }
211 }
212
213 pub fn new() -> Result<Self, AccessTokenError> {
214 let (api_key, api_secret) = get_env_keys()?;
216 Ok(Self::with_api_key(&api_key, &api_secret))
217 }
218
219 pub fn with_ttl(mut self, ttl: Duration) -> Self {
220 let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + ttl;
221 self.claims.exp = time.as_secs() as usize;
222 self
223 }
224
225 pub fn with_grants(mut self, grants: VideoGrants) -> Self {
226 self.claims.video = grants;
227 self
228 }
229
230 pub fn with_sip_grants(mut self, grants: SIPGrants) -> Self {
231 self.claims.sip = grants;
232 self
233 }
234
235 pub fn with_identity(mut self, identity: &str) -> Self {
236 self.claims.sub = identity.to_owned();
237 self
238 }
239
240 pub fn with_name(mut self, name: &str) -> Self {
241 self.claims.name = name.to_owned();
242 self
243 }
244
245 pub fn with_metadata(mut self, metadata: &str) -> Self {
246 self.claims.metadata = metadata.to_owned();
247 self
248 }
249
250 pub fn with_attributes<I, K, V>(mut self, attributes: I) -> Self
251 where
252 I: IntoIterator<Item = (K, V)>,
253 K: Into<String>,
254 V: Into<String>,
255 {
256 self.claims.attributes =
257 attributes.into_iter().map(|(k, v)| (k.into(), v.into())).collect::<HashMap<_, _>>();
258 self
259 }
260
261 pub fn with_sha256(mut self, sha256: &str) -> Self {
262 self.claims.sha256 = sha256.to_owned();
263 self
264 }
265
266 pub fn with_room_config(mut self, config: livekit_protocol::RoomConfiguration) -> Self {
267 self.claims.room_config = Some(config);
268 self
269 }
270
271 pub fn to_jwt(self) -> Result<String, AccessTokenError> {
272 if self.api_key.is_empty() || self.api_secret.is_empty() {
273 return Err(AccessTokenError::InvalidKeys);
274 }
275
276 if self.claims.video.room_join
277 && (self.claims.sub.is_empty() || self.claims.video.room.is_empty())
278 {
279 return Err(AccessTokenError::InvalidClaims(
280 "token grants room_join but doesn't have an identity or room",
281 ));
282 }
283
284 Ok(jsonwebtoken::encode(
285 &Header::new(jsonwebtoken::Algorithm::HS256),
286 &self.claims,
287 &EncodingKey::from_secret(self.api_secret.as_ref()),
288 )?)
289 }
290}
291
292#[derive(Clone)]
293pub struct TokenVerifier {
294 api_key: String,
295 api_secret: String,
296}
297
298impl Debug for TokenVerifier {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 f.debug_struct("TokenVerifier").field("api_key", &self.api_key).finish()
301 }
302}
303
304impl TokenVerifier {
305 pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
306 Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned() }
307 }
308
309 pub fn new() -> Result<Self, AccessTokenError> {
310 let (api_key, api_secret) = get_env_keys()?;
311 Ok(Self::with_api_key(&api_key, &api_secret))
312 }
313
314 pub fn verify(&self, token: &str) -> Result<Claims, AccessTokenError> {
315 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
316 validation.validate_exp = true;
317 validation.validate_nbf = true;
318 validation.set_issuer(&[&self.api_key]);
319
320 let token = jsonwebtoken::decode::<Claims>(
321 token,
322 &DecodingKey::from_secret(self.api_secret.as_ref()),
323 &validation,
324 )?;
325
326 Ok(token.claims)
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use std::time::Duration;
333
334 use super::{AccessToken, Claims, TokenVerifier, VideoGrants};
335
336 const TEST_API_KEY: &str = "myapikey";
337 const TEST_API_SECRET: &str = "thiskeyistotallyunsafe";
338 const TEST_TOKEN: &str = include_str!("test_token.txt");
339
340 #[test]
341 fn test_access_token() {
342 let room_config = livekit_protocol::RoomConfiguration {
343 name: "name".to_string(),
344 agents: vec![livekit_protocol::RoomAgentDispatch {
345 agent_name: "test-agent".to_string(),
346 metadata: "test-metadata".to_string(),
347 }],
348 ..Default::default()
349 };
350
351 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
352 .with_ttl(Duration::from_secs(60))
353 .with_identity("test")
354 .with_name("test")
355 .with_grants(VideoGrants::default())
356 .with_room_config(room_config.clone())
357 .to_jwt()
358 .unwrap();
359
360 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
361 let claims = verifier.verify(&token).unwrap();
362
363 assert_eq!(claims.sub, "test");
364 assert_eq!(claims.name, "test");
365 assert_eq!(claims.iss, TEST_API_KEY);
366 assert_eq!(claims.room_config, Some(room_config));
367
368 let incorrect_issuer = TokenVerifier::with_api_key("incorrect", TEST_API_SECRET);
369 assert!(incorrect_issuer.verify(&token).is_err());
370
371 let incorrect_token = TokenVerifier::with_api_key(TEST_API_KEY, "incorrect");
372 assert!(incorrect_token.verify(&token).is_err());
373 }
374
375 #[test]
376 fn test_verify_token_with_room_config() {
377 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
378 let claims = verifier.verify(TEST_TOKEN).expect("Failed to verify token.");
380
381 assert_eq!(
382 super::Claims {
383 sub: "identity".to_string(),
384 name: "name".to_string(),
385 room_config: Some(livekit_protocol::RoomConfiguration {
386 agents: vec![livekit_protocol::RoomAgentDispatch {
387 agent_name: "test-agent".to_string(),
388 metadata: "test-metadata".to_string(),
389 }],
390 ..Default::default()
391 }),
392 ..claims.clone()
393 },
394 claims
395 );
396 }
397
398 #[test]
399 fn test_unverified_token() {
400 let claims = Claims::from_unverified(TEST_TOKEN).expect("Failed to parse token");
401
402 assert_eq!(claims.sub, "identity");
403 assert_eq!(claims.name, "name");
404 assert_eq!(claims.iss, TEST_API_KEY);
405 assert_eq!(
406 claims.room_config,
407 Some(livekit_protocol::RoomConfiguration {
408 agents: vec![livekit_protocol::RoomAgentDispatch {
409 agent_name: "test-agent".to_string(),
410 metadata: "test-metadata".to_string(),
411 }],
412 ..Default::default()
413 })
414 );
415
416 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
417 .with_ttl(Duration::from_secs(60))
418 .with_identity("test")
419 .with_name("test")
420 .with_grants(VideoGrants {
421 room_join: true,
422 room: "test-room".to_string(),
423 ..Default::default()
424 })
425 .to_jwt()
426 .unwrap();
427
428 let claims = Claims::from_unverified(&token).expect("Failed to parse fresh token");
429 assert_eq!(claims.sub, "test");
430 assert_eq!(claims.name, "test");
431 assert_eq!(claims.video.room, "test-room");
432 assert!(claims.video.room_join);
433
434 let parts: Vec<&str> = token.split('.').collect();
435 let malformed_token = format!("{}.{}.wrongsignature", parts[0], parts[1]);
436
437 let claims = Claims::from_unverified(&malformed_token)
438 .expect("Failed to parse token with wrong signature");
439 assert_eq!(claims.sub, "test");
440 assert_eq!(claims.name, "test");
441 }
442}