diff --git a/livekit-api/livekit/api/__init__.py b/livekit-api/livekit/api/__init__.py index 5b669cbe..a8c69e05 100644 --- a/livekit-api/livekit/api/__init__.py +++ b/livekit-api/livekit/api/__init__.py @@ -22,6 +22,6 @@ from livekit.protocol import models from livekit.protocol import room -from .api import LiveKitAPI -from .access_token import VideoGrants, AccessToken +from .livekit_api import LiveKitAPI +from .access_token import VideoGrants, AccessToken, TokenVerifier from .version import __version__ diff --git a/livekit-api/livekit/api/access_token.py b/livekit-api/livekit/api/access_token.py index 6cad10de..cba3d9df 100644 --- a/livekit-api/livekit/api/access_token.py +++ b/livekit-api/livekit/api/access_token.py @@ -14,9 +14,9 @@ import calendar import dataclasses +import re import datetime import os - import jwt DEFAULT_TTL = datetime.timedelta(hours=6) @@ -64,6 +64,11 @@ class VideoGrants: @dataclasses.dataclass class Claims: + exp: int = 0 + iss: str = "" # api key + nbf: int = 0 + sub: str = "" # identity + name: str = "" video: VideoGrants = dataclasses.field(default_factory=VideoGrants) metadata: str = "" @@ -111,17 +116,12 @@ def with_sha256(self, sha256: str) -> "AccessToken": return self def to_jwt(self) -> str: - def camel_case_dict(data) -> dict: - return { - "".join( - word if i == 0 else word.title() - for i, word in enumerate(key.split("_")) - ): value - for key, value in data - if value is not None - } + video = self.claims.video + if video.room_join and (not self.identity or not video.room): + raise ValueError("identity and room must be set when joining a room") claims = dataclasses.asdict(self.claims) + claims = {camel_to_snake(k): v for k, v in claims.items()} claims.update( { "sub": self.identity, @@ -130,9 +130,6 @@ def camel_case_dict(data) -> dict: "exp": calendar.timegm( (datetime.datetime.utcnow() + self.ttl).utctimetuple() ), - "video": dataclasses.asdict( - self.claims.video, dict_factory=camel_case_dict - ), } ) @@ -144,9 +141,12 @@ def __init__( self, api_key: str = os.getenv("LIVEKIT_API_KEY", ""), api_secret: str = os.getenv("LIVEKIT_API_SECRET", ""), + *, + leeway: datetime.timedelta = DEFAULT_LEEWAY, ) -> None: self.api_key = api_key self.api_secret = api_secret + self._leeway = leeway def verify(self, token: str) -> Claims: claims = jwt.decode( @@ -154,6 +154,18 @@ def verify(self, token: str) -> Claims: self.api_secret, issuer=self.api_key, algorithms=["HS256"], - leeway=DEFAULT_LEEWAY.total_seconds(), + leeway=self._leeway.total_seconds(), ) - return Claims(**claims) + c = Claims(**claims) + + video = claims["video"] + video = {camel_to_snake(k): v for k, v in video.items()} + c.video = VideoGrants(**video) + return c + + +def camel_to_snake(str): + return re.sub(r'(? proto_egress.EgressInfo: return await self._client.request( SVC, @@ -24,7 +24,7 @@ async def start_room_composite_egress( ) async def start_web_egress( - self, start: proto_egress.StartWebEgressRequest + self, start: proto_egress.WebEgressRequest ) -> proto_egress.EgressInfo: return await self._client.request( SVC, diff --git a/livekit-api/livekit/api/api.py b/livekit-api/livekit/api/livekit_api.py similarity index 100% rename from livekit-api/livekit/api/api.py rename to livekit-api/livekit/api/livekit_api.py diff --git a/livekit-api/tests/test_access_token.py b/livekit-api/tests/test_access_token.py new file mode 100644 index 00000000..8572b4f0 --- /dev/null +++ b/livekit-api/tests/test_access_token.py @@ -0,0 +1,49 @@ +import pytest +import datetime +from livekit.api import AccessToken, TokenVerifier, VideoGrants + +TEST_API_KEY = "myapikey" +TEST_API_SECRET = "thiskeyistotallyunsafe" + + +def test_verify_token(): + grants = VideoGrants(room_join=True, room="test_room") + + token = ( + AccessToken(TEST_API_KEY, TEST_API_SECRET) + .with_identity("test_identity") + .with_metadata("test_metadata") + .with_grants(grants) + .to_jwt() + ) + + token_verifier = TokenVerifier(TEST_API_KEY, TEST_API_SECRET) + claims = token_verifier.verify(token) + + assert claims.sub == "test_identity" + assert claims.metadata == "test_metadata" + assert claims.video == grants + + +def test_verify_token_invalid(): + token = ( + AccessToken(TEST_API_KEY, TEST_API_SECRET) + .with_identity("test_identity") + .to_jwt() + ) + + token_verifier = TokenVerifier(TEST_API_KEY, "invalid_secret") + with pytest.raises(Exception): + token_verifier.verify(token) + +def test_verify_token_expired(): + token = ( + AccessToken(TEST_API_KEY, TEST_API_SECRET) + .with_identity("test_identity") + .with_ttl(datetime.timedelta(seconds=0)) + .to_jwt() + ) + + token_verifier = TokenVerifier(TEST_API_KEY, TEST_API_SECRET, leeway=datetime.timedelta(seconds=0)) + with pytest.raises(Exception): + token_verifier.verify(token)