Skip to content

Commit 613e536

Browse files
authored
Merge pull request #86 from davemun/jwt-auth
Implements JWT auth for REST API calls.
2 parents 0358a24 + 39ed62c commit 613e536

File tree

7 files changed

+56
-22
lines changed

7 files changed

+56
-22
lines changed

opentok/opentok.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import platform # user-agent
1111
from socket import inet_aton # create_session
1212
import xml.dom.minidom as xmldom # create_session
13+
from jose import jwt # _create_jwt_auth_header
14+
import random # _create_jwt_auth_header
1315

1416
# compat
1517
from six.moves.urllib.parse import urlencode
@@ -273,7 +275,7 @@ def headers(self):
273275
"""For internal use."""
274276
return {
275277
'User-Agent': 'OpenTok-Python-SDK/' + __version__ + ' ' + platform.python_version(),
276-
'X-TB-PARTNER-AUTH': self.api_key + ':' + self.api_secret
278+
'X-TB-OPENTOK-AUTH': self._create_jwt_auth_header()
277279
}
278280

279281
def archive_headers(self):
@@ -444,3 +446,13 @@ def get_archives(self, offset=None, count=None):
444446

445447
def _sign_string(self, string, secret):
446448
return hmac.new(secret.encode('utf-8'), string.encode('utf-8'), hashlib.sha1).hexdigest()
449+
450+
def _create_jwt_auth_header(self):
451+
payload = {
452+
'ist': 'project',
453+
'iss': self.api_key,
454+
'exp': int(time.time()) + (60*5), # 5 minutes
455+
'jti': '{:f}'.format(random.random())
456+
}
457+
458+
return jwt.encode(payload, self.api_secret, algorithm='HS256')

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def find_version(*file_paths):
3131
'requests',
3232
'six',
3333
'pytz',
34+
'python-jose'
3435
]
3536

3637
if sys.version_info[0] < 3 or sys.version_info[1] < 4:

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .validate_jwt import validate_jwt_header

tests/test_archive.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json
88
import datetime
99
import pytz
10+
from .validate_jwt import validate_jwt_header
1011

1112
from opentok import OpenTok, Archive, __version__, OutputModes
1213

@@ -56,7 +57,7 @@ def test_stop_archive(self):
5657

5758
archive.stop()
5859

59-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
60+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
6061
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
6162
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
6263
expect(archive).to.be.an(Archive)
@@ -91,7 +92,7 @@ def test_delete_archive(self):
9192
u('size'): 0,
9293
u('status'): u('available'),
9394
u('hasAudio'): True,
94-
u('hasVideo'): True,
95+
u('hasVideo'): True,
9596
u('outputMode'): OutputModes.composed.value,
9697
u('url'): None,
9798
})
@@ -101,7 +102,7 @@ def test_delete_archive(self):
101102

102103
archive.delete()
103104

104-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
105+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
105106
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
106107
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
107108
# TODO: test that the object is invalidated

tests/test_archive_api.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json
88
import datetime
99
import pytz
10+
from .validate_jwt import validate_jwt_header
1011

1112
from opentok import OpenTok, Archive, ArchiveList, OutputModes, __version__
1213

@@ -41,7 +42,7 @@ def test_start_archive(self):
4142

4243
archive = self.opentok.start_archive(self.session_id)
4344

44-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
45+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
4546
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
4647
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
4748
# non-deterministic json encoding. have to decode to test it properly
@@ -92,7 +93,7 @@ def test_start_archive_with_name(self):
9293

9394
archive = self.opentok.start_archive(self.session_id, name=u('ARCHIVE NAME'))
9495

95-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
96+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
9697
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
9798
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
9899
# non-deterministic json encoding. have to decode to test it properly
@@ -141,7 +142,7 @@ def test_start_voice_archive(self):
141142

142143
archive = self.opentok.start_archive(self.session_id, name=u('ARCHIVE NAME'), has_video=False)
143144

144-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
145+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
145146
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
146147
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
147148
# non-deterministic json encoding. have to decode to test it properly
@@ -192,7 +193,7 @@ def test_start_individual_archive(self):
192193

193194
archive = self.opentok.start_archive(self.session_id, name=u('ARCHIVE NAME'), output_mode=OutputModes.individual)
194195

195-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
196+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
196197
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
197198
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
198199
# non-deterministic json encoding. have to decode to test it properly
@@ -244,7 +245,7 @@ def test_start_composed_archive(self):
244245

245246
archive = self.opentok.start_archive(self.session_id, name=u('ARCHIVE NAME'), output_mode=OutputModes.composed)
246247

247-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
248+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
248249
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
249250
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
250251
# non-deterministic json encoding. have to decode to test it properly
@@ -297,7 +298,7 @@ def test_stop_archive(self):
297298

298299
archive = self.opentok.stop_archive(archive_id)
299300

300-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
301+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
301302
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
302303
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
303304
expect(archive).to.be.an(Archive)
@@ -324,7 +325,7 @@ def test_delete_archive(self):
324325

325326
self.opentok.delete_archive(archive_id)
326327

327-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
328+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
328329
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
329330
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
330331

@@ -353,7 +354,7 @@ def test_find_archive(self):
353354

354355
archive = self.opentok.get_archive(archive_id)
355356

356-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
357+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
357358
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
358359
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
359360
expect(archive).to.be.an(Archive)
@@ -468,7 +469,7 @@ def test_find_archives(self):
468469

469470
archive_list = self.opentok.get_archives()
470471

471-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
472+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
472473
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
473474
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
474475
expect(archive_list).to.be.an(ArchiveList)
@@ -528,7 +529,7 @@ def test_find_archives_with_offset(self):
528529

529530
archive_list = self.opentok.get_archives(offset=3)
530531

531-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
532+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
532533
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
533534
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
534535
expect(httpretty.last_request()).to.have.property("querystring").being.equal({
@@ -578,7 +579,7 @@ def test_find_archives_with_count(self):
578579

579580
archive_list = self.opentok.get_archives(count=2)
580581

581-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
582+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
582583
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
583584
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
584585
expect(httpretty.last_request()).to.have.property("querystring").being.equal({
@@ -654,7 +655,7 @@ def test_find_archives_with_offset_and_count(self):
654655

655656
archive_list = self.opentok.get_archives(count=4, offset=2)
656657

657-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
658+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
658659
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
659660
expect(httpretty.last_request().headers[u('content-type')]).to.equal(u('application/json'))
660661
expect(httpretty.last_request()).to.have.property("querystring").being.equal({

tests/test_session_creation.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from nose.tools import raises
55
from sure import expect
66
import httpretty
7+
from .validate_jwt import validate_jwt_header
78

89
from opentok import OpenTok, Session, MediaModes, ArchiveModes, OpenTokException, __version__
910

@@ -22,7 +23,7 @@ def test_create_default_session(self):
2223

2324
session = self.opentok.create_session()
2425

25-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
26+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
2627
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
2728
body = parse_qs(httpretty.last_request().body)
2829
expect(body).to.have.key(b('p2p.preference')).being.equal([b('enabled')])
@@ -41,7 +42,7 @@ def test_create_routed_session(self):
4142

4243
session = self.opentok.create_session(media_mode=MediaModes.routed)
4344

44-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
45+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
4546
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
4647
body = parse_qs(httpretty.last_request().body)
4748
expect(body).to.have.key(b('p2p.preference')).being.equal([b('disabled')])
@@ -60,7 +61,7 @@ def test_create_session_with_location_hint(self):
6061

6162
session = self.opentok.create_session(location='12.34.56.78')
6263

63-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
64+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
6465
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
6566
# ordering of keys is non-deterministic, must parse the body to see if it is correct
6667
body = parse_qs(httpretty.last_request().body)
@@ -80,7 +81,7 @@ def test_create_routed_session_with_location_hint(self):
8081

8182
session = self.opentok.create_session(location='12.34.56.78', media_mode=MediaModes.routed)
8283

83-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
84+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
8485
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
8586
# ordering of keys is non-deterministic, must parse the body to see if it is correct
8687
body = parse_qs(httpretty.last_request().body)
@@ -100,7 +101,7 @@ def test_create_manual_archive_mode_session(self):
100101

101102
session = self.opentok.create_session(media_mode=MediaModes.routed, archive_mode=ArchiveModes.manual)
102103

103-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
104+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
104105
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
105106
body = parse_qs(httpretty.last_request().body)
106107
expect(body).to.have.key(b('p2p.preference')).being.equal([b('disabled')])
@@ -119,7 +120,7 @@ def test_create_always_archive_mode_session(self):
119120

120121
session = self.opentok.create_session(media_mode=MediaModes.routed, archive_mode=ArchiveModes.always)
121122

122-
expect(httpretty.last_request().headers[u('x-tb-partner-auth')]).to.equal(self.api_key+u(':')+self.api_secret)
123+
validate_jwt_header(self, httpretty.last_request().headers[u('x-tb-opentok-auth')])
123124
expect(httpretty.last_request().headers[u('user-agent')]).to.contain(u('OpenTok-Python-SDK/')+__version__)
124125
body = parse_qs(httpretty.last_request().body)
125126
expect(body).to.have.key(b('p2p.preference')).being.equal([b('disabled')])

tests/validate_jwt.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from six import u
2+
from sure import expect
3+
from jose import jwt
4+
import time
5+
6+
def validate_jwt_header(self, jsonwebtoken):
7+
claims = jwt.decode(jsonwebtoken, self.api_secret, algorithms=[u('HS256')])
8+
claims.should.have.key(u('iss'))
9+
expect(claims[u('iss')]).to.equal(self.api_key)
10+
claims.should.have.key(u('ist'))
11+
expect(claims[u('ist')]).to.equal(u('project'))
12+
claims.should.have.key(u('exp'))
13+
expect(float(claims[u('exp')])).to.be.greater_than(float(time.time()))
14+
# todo: add test to check for anvil failure code if exp time is greater than anvil expects
15+
claims.should.have.key(u('jti'))
16+
expect(float(claims[u('jti')])).to.be.greater_than_or_equal_to(float(0))
17+
expect(float(claims[u('jti')])).to.be.lower_than(float(1))

0 commit comments

Comments
 (0)