Skip to main content

livekit_token/
access_token.rs

1// Copyright 2025 LiveKit, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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); // 6 hours
30
31#[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    // actions on rooms
47    #[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    // actions on a particular room
55    #[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    // permissions within a room
65    #[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    // TrackSource types that a participant may publish.
73    // When set, it supercedes CanPublish. Only sources explicitly set here can be published
74    #[serde(skip_serializing_if = "is_default")]
75    pub can_publish_sources: Vec<String>, // keys keep track of each source
76
77    // by default, a participant is not allowed to update its own metadata
78    #[serde(skip_serializing_if = "is_default")]
79    pub can_update_own_metadata: Option<bool>,
80
81    // actions on ingresses
82    #[serde(skip_serializing_if = "is_default")]
83    pub ingress_admin: bool, // applies to all ingress
84
85    // participant is not visible to other participants (useful when making bots)
86    #[serde(skip_serializing_if = "is_default")]
87    pub hidden: bool,
88
89    // indicates to the room that current participant is a recorder
90    #[serde(skip_serializing_if = "is_default")]
91    pub recorder: bool,
92
93    // indicates to the room that current participant is an agent
94    #[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    // manage sip resources
124    #[serde(default, skip_serializing_if = "is_default")]
125    pub admin: bool,
126    // make outbound calls
127    #[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,  // Expiration
142    pub iss: String, // ApiKey
143    pub nbf: usize,
144    #[serde(skip_serializing_if = "is_default")]
145    pub sub: String, // Identity
146
147    #[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, // Used to verify the integrity of the message body
157    #[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        // Don't show api_secret here
183        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        // Try to get the API Key and the Secret Key from the environment
220        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        // This token was generated using the Python SDK.
392        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}