Skip to content

Commit 678e6e7

Browse files
authored
Merge pull request #248 from opentok/add-jwt-generation
Add jwt generation
2 parents 4b5f8ff + c5a6ee3 commit 678e6e7

9 files changed

+99
-70
lines changed

.bumpversion.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.9.2
2+
current_version = 3.10.0
33
commit = True
44
tag = True
55

CHANGES.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Release 3.10.0
2+
- Add new `max_bitrate` option for archives
3+
- Change to create JWTs by default in the `Client.generate_token` method. T1 tokens can still be created by setting `use_jwt=False` when generating a token.
4+
15
# Release 3.9.2
26
- Migrate from using `python-jose` with native-python cryptographic backend to the `pyjwt` package
37

opentok/opentok.py

+40-12
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import time # generate_token
77
import hmac # _sign_string
88
import hashlib
9-
from typing import List # use for type hinting
9+
from typing import List
1010
import requests # create_session, archiving
1111
import json # archiving
1212
import platform # user-agent
@@ -174,6 +174,7 @@ def generate_token(
174174
expire_time=None,
175175
data=None,
176176
initial_layout_class_list=[],
177+
use_jwt=True,
177178
):
178179
"""
179180
Generates a token for a given session.
@@ -212,6 +213,9 @@ def generate_token(
212213
`live streaming broadcasts <https://tokbox.com/developer/guides/broadcast/#live-streaming>`_ and
213214
`composed archives <https://tokbox.com/developer/guides/archiving/layout-control.html>`_
214215
216+
:param bool use_jwt: Whether to use JWT tokens or not. If set to False, the token will be a
217+
plain text token. If set to True (the default), the token will be a JWT.
218+
215219
:rtype:
216220
The token string.
217221
"""
@@ -287,7 +291,7 @@ def generate_token(
287291
try:
288292
decoded_session_id = base64.b64decode(sub_session_id_bytes_padded, b("-_"))
289293
parts = decoded_session_id.decode("utf-8").split(u("~"))
290-
except Exception as e:
294+
except Exception:
291295
raise OpenTokException(
292296
u("Cannot generate token, the session_id {0} was not valid").format(
293297
session_id
@@ -300,6 +304,29 @@ def generate_token(
300304
).format(session_id, self.api_key)
301305
)
302306

307+
if use_jwt:
308+
payload = {}
309+
payload['iss'] = self.api_key
310+
payload['ist'] = 'project'
311+
payload['iat'] = now
312+
payload["exp"] = expire_time
313+
payload['nonce'] = random.randint(0, 999999)
314+
payload['role'] = role.value
315+
payload['scope'] = 'session.connect'
316+
payload['session_id'] = session_id
317+
if initial_layout_class_list:
318+
payload['initial_layout_class_list'] = (
319+
initial_layout_class_list_serialized
320+
)
321+
if data:
322+
payload['connection_data'] = data
323+
324+
headers = {'alg': 'HS256', 'typ': 'JWT'}
325+
326+
token = encode(payload, self.api_secret, algorithm="HS256", headers=headers)
327+
328+
return token
329+
303330
data_params = dict(
304331
session_id=session_id,
305332
create_time=now,
@@ -322,6 +349,7 @@ def generate_token(
322349
sentinal=self.TOKEN_SENTINEL,
323350
base64_data=base64.b64encode(decoded_base64_bytes).decode(),
324351
)
352+
325353
return token
326354

327355
def create_session(
@@ -470,7 +498,7 @@ def create_session(
470498
try:
471499
logger.debug(
472500
"POST to %r with params %r, headers %r, proxies %r",
473-
self.endpoints.session_url(),
501+
self.endpoints.get_session_url(),
474502
options,
475503
self.get_headers(),
476504
self.proxies,
@@ -654,7 +682,7 @@ def start_archive(
654682

655683
logger.debug(
656684
"POST to %r with params %r, headers %r, proxies %r",
657-
self.endpoints.archive_url(),
685+
self.endpoints.get_archive_url(),
658686
json.dumps(payload),
659687
self.get_json_headers(),
660688
self.proxies,
@@ -701,7 +729,7 @@ def stop_archive(self, archive_id):
701729
"""
702730
logger.debug(
703731
"POST to %r with headers %r, proxies %r",
704-
self.endpoints.archive_url(archive_id) + "/stop",
732+
self.endpoints.get_archive_url(archive_id) + "/stop",
705733
self.get_json_headers(),
706734
self.proxies,
707735
)
@@ -736,7 +764,7 @@ def delete_archive(self, archive_id):
736764
"""
737765
logger.debug(
738766
"DELETE to %r with headers %r, proxies %r",
739-
self.endpoints.archive_url(archive_id),
767+
self.endpoints.get_archive_url(archive_id),
740768
self.get_json_headers(),
741769
self.proxies,
742770
)
@@ -766,7 +794,7 @@ def get_archive(self, archive_id):
766794
"""
767795
logger.debug(
768796
"GET to %r with headers %r, proxies %r",
769-
self.endpoints.archive_url(archive_id),
797+
self.endpoints.get_archive_url(archive_id),
770798
self.get_json_headers(),
771799
self.proxies,
772800
)
@@ -959,7 +987,7 @@ def send_signal(self, session_id, payload, connection_id=None):
959987
"""
960988
logger.debug(
961989
"POST to %r with params %r, headers %r, proxies %r",
962-
self.endpoints.signaling_url(session_id, connection_id),
990+
self.endpoints.get_signaling_url(session_id, connection_id),
963991
json.dumps(payload),
964992
self.get_json_headers(),
965993
self.proxies,
@@ -1456,7 +1484,7 @@ def start_broadcast(self, session_id, options, stream_mode=BroadcastStreamModes.
14561484

14571485
payload.update(options)
14581486

1459-
endpoint = self.endpoints.broadcast_url()
1487+
endpoint = self.endpoints.get_broadcast_url()
14601488

14611489
logger.debug(
14621490
"POST to %r with params %r, headers %r, proxies %r",
@@ -1500,7 +1528,7 @@ def stop_broadcast(self, broadcast_id):
15001528
projectId, createdAt, updatedAt and resolution
15011529
"""
15021530

1503-
endpoint = self.endpoints.broadcast_url(broadcast_id, stop=True)
1531+
endpoint = self.endpoints.get_broadcast_url(broadcast_id, stop=True)
15041532

15051533
logger.debug(
15061534
"POST to %r with headers %r, proxies %r",
@@ -1639,7 +1667,7 @@ def get_broadcast(self, broadcast_id):
16391667
projectId, createdAt, updatedAt, resolution, broadcastUrls and status
16401668
"""
16411669

1642-
endpoint = self.endpoints.broadcast_url(broadcast_id)
1670+
endpoint = self.endpoints.get_broadcast_url(broadcast_id)
16431671

16441672
logger.debug(
16451673
"GET to %r with headers %r, proxies %r",
@@ -1697,7 +1725,7 @@ def set_broadcast_layout(
16971725
if stylesheet is not None:
16981726
payload["stylesheet"] = stylesheet
16991727

1700-
endpoint = self.endpoints.broadcast_url(broadcast_id, layout=True)
1728+
endpoint = self.endpoints.get_broadcast_url(broadcast_id, layout=True)
17011729

17021730
logger.debug(
17031731
"PUT to %r with params %r, headers %r, proxies %r",

opentok/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers
2-
__version__ = "3.9.2"
2+
__version__ = "3.10.0"
33

sample/HelloWorld/helloworld.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@
1717
def hello():
1818
key = api_key
1919
session_id = session.session_id
20-
token = opentok.generate_token(session_id)
21-
return render_template(
22-
"index.html", api_key=key, session_id=session_id, token=token
23-
)
20+
token = opentok.generate_token(session_id, use_jwt=True)
21+
return render_template("index.html", api_key=key, session_id=session_id, token=token)
2422

2523

2624
if __name__ == "__main__":

tests/helpers.py

+22-16
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
1-
from six import text_type, u, b, PY3
1+
from six import u, PY3
22
from six.moves.urllib.parse import parse_qs
33
import base64
44
import hmac
55
import hashlib
6+
from jwt import decode
67

78

8-
def token_decoder(token):
9+
def token_decoder(token: str, secret: str = None):
910
token_data = {}
10-
# remove sentinal
11-
encoded = token[4:]
12-
decoded = base64.b64decode(encoded.encode("utf-8"))
13-
# decode the bytes object back to unicode with utf-8 encoding
14-
if PY3:
15-
decoded = decoded.decode()
16-
parts = decoded.split(u(":"))
17-
for decoded_part in iter(parts):
18-
token_data.update(parse_qs(decoded_part))
19-
# TODO: probably a more elegent way
20-
for k in iter(token_data):
21-
token_data[k] = token_data[k][0]
22-
token_data[u("data_string")] = parts[1]
23-
return token_data
11+
if token.startswith("T1=="):
12+
encoded = token[4:]
13+
14+
# decode the token from base64
15+
decoded = base64.b64decode(encoded.encode("utf-8"))
16+
# decode the bytes object back to unicode with utf-8 encoding
17+
if PY3:
18+
decoded = decoded.decode()
19+
parts = decoded.split(u(":"))
20+
for decoded_part in iter(parts):
21+
token_data.update(parse_qs(decoded_part))
22+
# TODO: probably a more elegant way
23+
for k in iter(token_data):
24+
token_data[k] = token_data[k][0]
25+
token_data[u("data_string")] = parts[1]
26+
return token_data
27+
28+
encoded = token.replace('Bearer ', '').strip()
29+
return decode(encoded, secret, algorithms='HS256')
2430

2531

2632
def token_signature_validator(token, secret):

tests/test_http_options.py

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def setUp(self):
2727
def tearDown(self):
2828
httpretty.disable()
2929

30+
@pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning")
3031
def test_timeout(self):
3132
with pytest.raises(OpenTokException):
3233
opentok = Client(self.api_key, self.api_secret, timeout=1)

tests/test_session.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import unittest
2-
from six import text_type, u, b, PY2, PY3
2+
from six import text_type, u
33

44
from opentok import Client, Session, Roles, MediaModes
5-
from .helpers import token_decoder, token_signature_validator
5+
from .helpers import token_decoder
66

77

88
class SessionTest(unittest.TestCase):
@@ -20,15 +20,13 @@ def test_generate_token(self):
2020
)
2121
token = session.generate_token()
2222
assert isinstance(token, text_type)
23-
assert token_decoder(token)[u("session_id")] == self.session_id
24-
assert token_signature_validator(token, self.api_secret)
23+
assert token_decoder(token, self.api_secret)[u("session_id")] == self.session_id
2524

2625
def test_generate_role_token(self):
2726
session = Session(
2827
self.opentok, self.session_id, media_mode=MediaModes.routed, location=None
2928
)
3029
token = session.generate_token(role=Roles.moderator)
3130
assert isinstance(token, text_type)
32-
assert token_decoder(token)[u("session_id")] == self.session_id
33-
assert token_decoder(token)[u("role")] == u("moderator")
34-
assert token_signature_validator(token, self.api_secret)
31+
assert token_decoder(token, self.api_secret)[u("session_id")] == self.session_id
32+
assert token_decoder(token, self.api_secret)[u("role")] == u("moderator")

tests/test_token_generation.py

+23-29
Original file line numberDiff line numberDiff line change
@@ -20,62 +20,56 @@ def setUp(self):
2020
)
2121
self.opentok = Client(self.api_key, self.api_secret)
2222

23-
def test_generate_plain_token(self):
24-
token = self.opentok.generate_token(self.session_id)
23+
def test_generate_plain_token_t1(self):
24+
token = self.opentok.generate_token(self.session_id, use_jwt=False)
2525
assert isinstance(token, text_type)
2626
assert token_decoder(token)[u("session_id")] == self.session_id
2727
assert token_signature_validator(token, self.api_secret)
2828

29+
def test_generate_plain_token_jwt(self):
30+
token = self.opentok.generate_token(self.session_id)
31+
assert isinstance(token, text_type)
32+
assert token_decoder(token, self.api_secret)[u("session_id")] == self.session_id
33+
2934
def test_generate_role_token(self):
3035
token = self.opentok.generate_token(self.session_id, Roles.moderator)
3136
assert isinstance(token, text_type)
32-
assert token_decoder(token)[u("role")] == Roles.moderator.value
33-
assert token_signature_validator(token, self.api_secret)
37+
assert token_decoder(token, self.api_secret)[u("role")] == Roles.moderator.value
3438

3539
token = self.opentok.generate_token(self.session_id, role=Roles.moderator)
3640
assert isinstance(token, text_type)
37-
assert token_decoder(token)[u("role")] == Roles.moderator.value
38-
assert token_signature_validator(token, self.api_secret)
41+
assert token_decoder(token, self.api_secret)[u("role")] == Roles.moderator.value
3942

4043
token = self.opentok.generate_token(self.session_id, Roles.publisher_only)
41-
assert token_decoder(token)["role"] == Roles.publisher_only.value
42-
assert token_signature_validator(token, self.api_secret)
44+
assert token_decoder(token, self.api_secret)["role"] == Roles.publisher_only.value
4345

4446
def test_generate_expires_token(self):
4547
# an integer is a valid argument
4648
expire_time = int(time.time()) + 100
4749
token = self.opentok.generate_token(self.session_id, expire_time=expire_time)
4850
assert isinstance(token, text_type)
49-
assert token_decoder(token)[u("expire_time")] == text_type(expire_time)
50-
assert token_signature_validator(token, self.api_secret)
51+
print(token_decoder(token, self.api_secret))
52+
assert token_decoder(token, self.api_secret)[u("exp")] == expire_time
5153
# anything that can be coerced into an integer is also valid
5254
expire_time = text_type(int(time.time()) + 100)
5355
token = self.opentok.generate_token(self.session_id, expire_time=expire_time)
5456
assert isinstance(token, text_type)
55-
assert token_decoder(token)[u("expire_time")] == expire_time
56-
assert token_signature_validator(token, self.api_secret)
57+
assert token_decoder(token, self.api_secret)[u("exp")] == int(expire_time)
5758
# a datetime object is also valid
58-
if PY2:
59-
expire_time = datetime.datetime.fromtimestamp(
60-
time.time(), pytz.UTC
61-
) + datetime.timedelta(days=1)
62-
if PY3:
63-
expire_time = datetime.datetime.fromtimestamp(
64-
time.time(), datetime.timezone.utc
65-
) + datetime.timedelta(days=1)
59+
expire_time = datetime.datetime.fromtimestamp(
60+
time.time(), datetime.timezone.utc
61+
) + datetime.timedelta(days=1)
6662
token = self.opentok.generate_token(self.session_id, expire_time=expire_time)
6763
assert isinstance(token, text_type)
68-
assert token_decoder(token)[u("expire_time")] == text_type(
69-
calendar.timegm(expire_time.utctimetuple())
64+
assert token_decoder(token, self.api_secret)[u("exp")] == calendar.timegm(
65+
expire_time.utctimetuple()
7066
)
71-
assert token_signature_validator(token, self.api_secret)
7267

7368
def test_generate_data_token(self):
7469
data = u("name=Johnny")
7570
token = self.opentok.generate_token(self.session_id, data=data)
7671
assert isinstance(token, text_type)
77-
assert token_decoder(token)[u("connection_data")] == data
78-
assert token_signature_validator(token, self.api_secret)
72+
assert token_decoder(token, self.api_secret)[u("connection_data")] == data
7973

8074
def test_generate_initial_layout_class_list(self):
8175
initial_layout_class_list = [u("focus"), u("small")]
@@ -84,15 +78,15 @@ def test_generate_initial_layout_class_list(self):
8478
)
8579
assert isinstance(token, text_type)
8680
assert sorted(
87-
token_decoder(token)[u("initial_layout_class_list")].split(u(" "))
81+
token_decoder(token, self.api_secret)[u("initial_layout_class_list")].split(
82+
u(" ")
83+
)
8884
) == sorted(initial_layout_class_list)
89-
assert token_signature_validator(token, self.api_secret)
9085

9186
def test_generate_no_data_token(self):
9287
token = self.opentok.generate_token(self.session_id)
9388
assert isinstance(token, text_type)
94-
assert u("connection_data") not in token_decoder(token)
95-
assert token_signature_validator(token, self.api_secret)
89+
assert u("connection_data") not in token_decoder(token, self.api_secret)
9690

9791
def test_does_not_generate_token_without_params(self):
9892
with pytest.raises(TypeError):

0 commit comments

Comments
 (0)