Skip to main content

livekit_api/
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, Serialize, Deserialize, PartialEq)]
44#[serde(rename_all = "camelCase")]
45pub struct VideoGrants {
46    // actions on rooms
47    #[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    // actions on a particular room
55    #[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    // permissions within a room
65    #[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    // TrackSource types that a participant may publish.
73    // When set, it supercedes CanPublish. Only sources explicitly set here can be published
74    #[serde(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(default)]
79    pub can_update_own_metadata: bool,
80
81    // actions on ingresses
82    #[serde(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(default)]
87    pub hidden: bool,
88
89    // indicates to the room that current participant is a recorder
90    #[serde(default)]
91    pub recorder: bool,
92}
93
94/// Used for fields that default to true instead of using the `Default` trait.
95fn 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    // manage sip resources
125    pub admin: bool,
126    // make outbound calls
127    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,  // Expiration
141    pub iss: String, // ApiKey
142    pub nbf: usize,
143    pub sub: String, // Identity
144
145    pub name: String,
146    pub video: VideoGrants,
147    pub sip: SIPGrants,
148    pub sha256: String, // Used to verify the integrity of the message body
149    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        // Don't show api_secret here
179        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        // Try to get the API Key and the Secret Key from the environment
215        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        // This token was generated using the Python SDK.
379        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}