Skip to content

Commit b5f007d

Browse files
authored
Merge pull request #306 from twilio/next-gen-jwt
Unify JWT based helpers
2 parents f59d463 + f59c211 commit b5f007d

File tree

17 files changed

+1175
-832
lines changed

17 files changed

+1175
-832
lines changed

.travis.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ python:
55
- "3.3"
66
- "3.4"
77
install:
8-
- pip install .
9-
- pip install -r requirements.txt
10-
- pip install -r tests/requirements.txt
8+
- make install
9+
- make test-install
1110
script:
12-
- make ci
11+
- make test

Makefile

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,12 @@ develop: venv
1515

1616
analysis:
1717
. venv/bin/activate; flake8 --ignore=E123,E126,E128,E501,W391,W291,W293,F401 tests
18-
. venv/bin/activate; flake8 --ignore=F401,W391,W291,W293 twilio --max-line-length=300
18+
. venv/bin/activate; flake8 --ignore=E402,F401,W391,W291,W293 twilio --max-line-length=300
1919

2020
test: analysis
2121
. venv/bin/activate; \
2222
find tests -type d | xargs nosetests
2323

24-
ci:
25-
flake8 --ignore=E123,E126,E128,E501,W391,W291,W293,F401 tests
26-
flake8 --ignore=F401,W391,W291,W293 twilio --max-line-length=300
27-
find tests -type d | xargs nosetests
28-
2924
cover:
3025
. venv/bin/activate; \
3126
find tests -type d | xargs nosetests --with-coverage --cover-inclusive --cover-erase --cover-package=twilio

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
six
22
httplib2
33
socksipy-branch
4+
PyJWT==1.4.2

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
#
1414
# You need to have the setuptools module installed. Try reading the setuptools
1515
# documentation: http://pypi.python.org/pypi/setuptools
16-
REQUIRES = ["httplib2 >= 0.7", "six", "pytz"]
16+
REQUIRES = ["httplib2 >= 0.7", "six", "pytz", "PyJWT == 1.4.2"]
1717

1818
if sys.version_info < (2, 6):
1919
REQUIRES.append('simplejson')

tests/unit/jwt/test_access_token.py

Lines changed: 86 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33

44
from datetime import datetime
55
from nose.tools import assert_equal
6-
from twilio.jwt import decode
7-
from twilio.jwt.access_token import AccessToken, ConversationsGrant, IpMessagingGrant, SyncGrant, VoiceGrant, VideoGrant
6+
7+
from twilio.jwt.access_token import AccessToken
8+
from twilio.jwt.access_token.grants import (
9+
ConversationsGrant,
10+
IpMessagingGrant,
11+
SyncGrant,
12+
VoiceGrant,
13+
VideoGrant
14+
)
815

916
ACCOUNT_SID = 'AC123'
1017
SIGNING_KEY_SID = 'SK123'
@@ -38,102 +45,109 @@ def _validate_claims(self, payload):
3845

3946
def test_empty_grants(self):
4047
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret')
41-
token = str(scat)
48+
token = scat.to_jwt()
4249

4350
assert_is_not_none(token)
44-
payload = decode(token, 'secret')
45-
self._validate_claims(payload)
46-
assert_equal({}, payload['grants'])
51+
decoded_token = AccessToken.from_jwt(token, 'secret')
52+
self._validate_claims(decoded_token.payload)
53+
assert_equal({}, decoded_token.payload['grants'])
4754

4855
def test_nbf(self):
4956
now = int(time.mktime(datetime.now().timetuple()))
5057
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret', nbf=now)
51-
token = str(scat)
58+
token = scat.to_jwt()
59+
60+
assert_is_not_none(token)
61+
decoded_token = AccessToken.from_jwt(token, 'secret')
62+
self._validate_claims(decoded_token.payload)
63+
assert_equal(now, decoded_token.nbf)
5264

65+
def test_headers(self):
66+
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret')
67+
token = scat.to_jwt()
5368
assert_is_not_none(token)
54-
payload = decode(token, 'secret')
55-
self._validate_claims(payload)
56-
assert_equal(now, payload['nbf'])
69+
decoded_token = AccessToken.from_jwt(token, 'secret')
70+
self.assertEqual(decoded_token.headers['cty'], 'twilio-fpa;v=1')
5771

5872
def test_identity(self):
5973
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret', identity='test@twilio.com')
60-
token = str(scat)
74+
token = scat.to_jwt()
6175

6276
assert_is_not_none(token)
63-
payload = decode(token, 'secret')
64-
self._validate_claims(payload)
77+
decoded_token = AccessToken.from_jwt(token, 'secret')
78+
self._validate_claims(decoded_token.payload)
6579
assert_equal({
6680
'identity': 'test@twilio.com'
67-
}, payload['grants'])
81+
}, decoded_token.payload['grants'])
6882

6983
def test_conversations_grant(self):
7084
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret')
7185
scat.add_grant(ConversationsGrant(configuration_profile_sid='CP123'))
7286

73-
token = str(scat)
87+
token = scat.to_jwt()
7488
assert_is_not_none(token)
75-
payload = decode(token, 'secret')
76-
self._validate_claims(payload)
77-
assert_equal(1, len(payload['grants']))
89+
decoded_token = AccessToken.from_jwt(token, 'secret')
90+
self._validate_claims(decoded_token.payload)
91+
assert_equal(1, len(decoded_token.payload['grants']))
7892
assert_equal({
7993
'configuration_profile_sid': 'CP123'
80-
}, payload['grants']['rtc'])
94+
}, decoded_token.payload['grants']['rtc'])
8195

8296
def test_video_grant(self):
8397
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret')
8498
scat.add_grant(VideoGrant(configuration_profile_sid='CP123'))
8599

86-
token = str(scat)
100+
token = scat.to_jwt()
87101
assert_is_not_none(token)
88-
payload = decode(token, 'secret')
89-
self._validate_claims(payload)
90-
assert_equal(1, len(payload['grants']))
102+
decoded_token = AccessToken.from_jwt(token, 'secret')
103+
self._validate_claims(decoded_token.payload)
104+
assert_equal(1, len(decoded_token.payload['grants']))
91105
assert_equal({
92106
'configuration_profile_sid': 'CP123'
93-
}, payload['grants']['video'])
107+
}, decoded_token.payload['grants']['video'])
94108

95109
def test_ip_messaging_grant(self):
96110
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret')
97111
scat.add_grant(IpMessagingGrant(service_sid='IS123', push_credential_sid='CR123'))
98112

99-
token = str(scat)
113+
token = scat.to_jwt()
100114
assert_is_not_none(token)
101-
payload = decode(token, 'secret')
102-
self._validate_claims(payload)
103-
assert_equal(1, len(payload['grants']))
115+
decoded_token = AccessToken.from_jwt(token, 'secret')
116+
self._validate_claims(decoded_token.payload)
117+
assert_equal(1, len(decoded_token.payload['grants']))
104118
assert_equal({
105119
'service_sid': 'IS123',
106120
'push_credential_sid': 'CR123'
107-
}, payload['grants']['ip_messaging'])
121+
}, decoded_token.payload['grants']['ip_messaging'])
108122

109123
def test_sync_grant(self):
110124
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret')
111125
scat.identity = "bender"
112126
scat.add_grant(SyncGrant(service_sid='IS123', endpoint_id='blahblahendpoint'))
113127

114-
token = str(scat)
128+
token = scat.to_jwt()
115129
assert_is_not_none(token)
116-
payload = decode(token, 'secret')
117-
self._validate_claims(payload)
118-
assert_equal(2, len(payload['grants']))
119-
assert_equal("bender", payload['grants']['identity'])
130+
decoded_token = AccessToken.from_jwt(token, 'secret')
131+
self._validate_claims(decoded_token.payload)
132+
assert_equal(2, len(decoded_token.payload['grants']))
133+
assert_equal("bender", decoded_token.payload['grants']['identity'])
120134
assert_equal({
121135
'service_sid': 'IS123',
122136
'endpoint_id': 'blahblahendpoint'
123-
}, payload['grants']['data_sync'])
137+
}, decoded_token.payload['grants']['data_sync'])
124138

125139
def test_grants(self):
126140
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret')
127141
scat.add_grant(ConversationsGrant())
128142
scat.add_grant(IpMessagingGrant())
129143

130-
token = str(scat)
144+
token = scat.to_jwt()
131145
assert_is_not_none(token)
132-
payload = decode(token, 'secret')
133-
self._validate_claims(payload)
134-
assert_equal(2, len(payload['grants']))
135-
assert_equal({}, payload['grants']['rtc'])
136-
assert_equal({}, payload['grants']['ip_messaging'])
146+
decoded_token = AccessToken.from_jwt(token, 'secret')
147+
self._validate_claims(decoded_token.payload)
148+
assert_equal(2, len(decoded_token.payload['grants']))
149+
assert_equal({}, decoded_token.payload['grants']['rtc'])
150+
assert_equal({}, decoded_token.payload['grants']['ip_messaging'])
137151

138152
def test_programmable_voice_grant(self):
139153
grant = VoiceGrant(
@@ -146,16 +160,42 @@ def test_programmable_voice_grant(self):
146160
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret')
147161
scat.add_grant(grant)
148162

149-
token = str(scat)
163+
token = scat.to_jwt()
150164
assert_is_not_none(token)
151-
payload = decode(token, 'secret')
152-
self._validate_claims(payload)
153-
assert_equal(1, len(payload['grants']))
165+
decoded_token = AccessToken.from_jwt(token, 'secret')
166+
self._validate_claims(decoded_token.payload)
167+
assert_equal(1, len(decoded_token.payload['grants']))
154168
assert_equal({
155169
'outgoing': {
156170
'application_sid': 'AP123',
157171
'params': {
158172
'foo': 'bar'
159173
}
160174
}
161-
}, payload['grants']['voice'])
175+
}, decoded_token.payload['grants']['voice'])
176+
177+
def test_pass_grants_in_constructor(self):
178+
grants = [
179+
ConversationsGrant(),
180+
IpMessagingGrant()
181+
]
182+
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret', grants=grants)
183+
184+
token = scat.to_jwt()
185+
assert_is_not_none(token)
186+
187+
decoded_token = AccessToken.from_jwt(token, 'secret')
188+
self._validate_claims(decoded_token.payload)
189+
assert_equal(2, len(decoded_token.payload['grants']))
190+
assert_equal({}, decoded_token.payload['grants']['rtc'])
191+
assert_equal({}, decoded_token.payload['grants']['ip_messaging'])
192+
193+
def test_constructor_validates_grants(self):
194+
grants = [ConversationsGrant, 'GrantMeAccessToEverything']
195+
self.assertRaises(ValueError, AccessToken, ACCOUNT_SID, SIGNING_KEY_SID, 'secret',
196+
grants=grants)
197+
198+
def test_add_grant_validates_grant(self):
199+
scat = AccessToken(ACCOUNT_SID, SIGNING_KEY_SID, 'secret')
200+
scat.add_grant(ConversationsGrant())
201+
self.assertRaises(ValueError, scat.add_grant, 'GrantRootAccess')

tests/unit/jwt/test_client.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import unittest
2+
3+
import time
4+
from nose.tools import assert_true, assert_equal
5+
6+
from twilio.jwt import Jwt
7+
from twilio.jwt.client import ClientCapabilityToken, ScopeURI
8+
9+
10+
class ClientCapabilityTokenTest(unittest.TestCase):
11+
12+
def assertIn(self, foo, bar, msg=None):
13+
"""backport for 2.6"""
14+
return assert_true(foo in bar, msg=(msg or "%s not found in %s" % (foo, bar)))
15+
16+
def now(self):
17+
return int(time.time())
18+
19+
def test_no_permissions(self):
20+
token = ClientCapabilityToken("AC123", "XXXXX")
21+
assert_equal(len(token._generate_payload()), 1)
22+
assert_equal(token._generate_payload()["scope"], '')
23+
24+
def test_inbound_permissions(self):
25+
token = ClientCapabilityToken("AC123", "XXXXX")
26+
token.allow_client_incoming("andy")
27+
28+
eurl = "scope:client:incoming?clientName=andy"
29+
assert_equal(len(token._generate_payload()), 1)
30+
assert_equal(token._generate_payload()['scope'], eurl)
31+
32+
def test_outbound_permissions(self):
33+
token = ClientCapabilityToken("AC123", "XXXXX")
34+
token.allow_client_outgoing("AP123")
35+
36+
eurl = "scope:client:outgoing?appSid=AP123"
37+
38+
assert_equal(len(token._generate_payload()), 1)
39+
self.assertIn(eurl, token._generate_payload()['scope'])
40+
41+
def test_outbound_permissions_params(self):
42+
token = ClientCapabilityToken("AC123", "XXXXX")
43+
token.allow_client_outgoing("AP123", foobar=3)
44+
45+
eurl = "scope:client:outgoing?appParams=foobar%3D3&appSid=AP123"
46+
assert_equal(token.payload["scope"], eurl)
47+
48+
def test_events(self):
49+
token = ClientCapabilityToken("AC123", "XXXXX")
50+
token.allow_event_stream()
51+
52+
event_uri = "scope:stream:subscribe?path=%2F2010-04-01%2FEvents"
53+
assert_equal(token.payload["scope"], event_uri)
54+
55+
def test_events_with_filters(self):
56+
token = ClientCapabilityToken("AC123", "XXXXX")
57+
token.allow_event_stream(foobar="hey")
58+
59+
event_uri = "scope:stream:subscribe?params=foobar%3Dhey&path=%2F2010-04-01%2FEvents"
60+
assert_equal(token.payload["scope"], event_uri)
61+
62+
def test_decode(self):
63+
token = ClientCapabilityToken("AC123", "XXXXX")
64+
token.allow_client_outgoing("AP123", foobar=3)
65+
token.allow_client_incoming("andy")
66+
token.allow_event_stream()
67+
68+
outgoing_uri = "scope:client:outgoing?appParams=foobar%3D3&appSid=AP123&clientName=andy"
69+
incoming_uri = "scope:client:incoming?clientName=andy"
70+
event_uri = "scope:stream:subscribe?path=%2F2010-04-01%2FEvents"
71+
72+
result = Jwt.from_jwt(token.to_jwt(), "XXXXX")
73+
scope = result.payload["scope"].split(" ")
74+
75+
self.assertIn(outgoing_uri, scope)
76+
self.assertIn(incoming_uri, scope)
77+
self.assertIn(event_uri, scope)
78+
79+
def test_encode_full_payload(self):
80+
token = ClientCapabilityToken("AC123", "XXXXX")
81+
token.allow_event_stream(foobar="hey")
82+
token.allow_client_incoming("andy")
83+
84+
event_uri = "scope:stream:subscribe?params=foobar%3Dhey&path=%2F2010-04-01%2FEvents"
85+
incoming_uri = "scope:client:incoming?clientName=andy"
86+
87+
self.assertIn(event_uri, token.payload["scope"])
88+
self.assertIn(incoming_uri, token.payload["scope"])
89+
self.assertEqual(token.payload['iss'], 'AC123')
90+
self.assertGreaterEqual(token.payload['exp'], self.now())
91+
92+
def test_pass_scopes_in_constructor(self):
93+
token = ClientCapabilityToken('AC123', 'XXXXX', allow_client_outgoing={
94+
'application_sid': 'AP123',
95+
'param1': 'val1'
96+
})
97+
outgoing_uri = "scope:client:outgoing?appParams=param1%3Dval1&appSid=AP123"
98+
result = Jwt.from_jwt(token.to_jwt(), "XXXXX")
99+
self.assertEqual(outgoing_uri, result.payload["scope"])
100+
101+
102+
class ScopeURITest(unittest.TestCase):
103+
104+
def test_to_payload_no_params(self):
105+
scope_uri = ScopeURI('service', 'godmode')
106+
self.assertEqual('scope:service:godmode', scope_uri.to_payload())
107+
108+
def test_to_payload_with_params(self):
109+
scope_uri = ScopeURI('service', 'godmode', {'key': 'val'})
110+
self.assertEqual('scope:service:godmode?key=val', scope_uri.to_payload())
111+
112+
def test_to_payload_with_params_encoded(self):
113+
scope_uri = ScopeURI('service', 'godmode', {'key with space': 'val'})
114+
self.assertEqual('scope:service:godmode?key+with+space=val', scope_uri.to_payload())

0 commit comments

Comments
 (0)