Skip to content

Commit 23d1293

Browse files
committed
Implement Audience and Issuer claims
1 parent 19da941 commit 23d1293

File tree

3 files changed

+199
-4
lines changed

3 files changed

+199
-4
lines changed

README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ used. PyJWT supports these reserved claim names:
9494

9595
- "exp" (Expiration Time) Claim
9696
- "nbf" (Not Before Time) Claim
97+
- "iss" (Issuer) Claim
98+
- "aud" (Audience) Claim
9799

98100
### Expiration Time Claim
99101

@@ -156,7 +158,51 @@ time.sleep(32)
156158
jwt.decode(jwt_payload, 'secret', leeway=10)
157159
```
158160

159-
PyJWT also supports not-before validation via the [`nbf` claim](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-27#section-4.1.5) in a similar fashion.
161+
### Not Before Time Claim
162+
163+
> The nbf (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the nbf claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the nbf claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL.
164+
165+
The `nbf` claim works similarly to the `exp` claim above.
166+
167+
```python
168+
jwt.encode({'nbf': 1371720939}, 'secret')
169+
170+
jwt.encode({'nbf': datetime.utcnow()}, 'secret')
171+
```
172+
173+
### Issuer Claim
174+
175+
> The iss (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The iss value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL.
176+
177+
```python
178+
import jwt
179+
180+
181+
payload = {
182+
'some': 'payload',
183+
'iss': 'urn:foo'
184+
}
185+
186+
token = jwt.encode(payload, 'secret')
187+
decoded = jwt.decode(token, 'secret', issuer='urn:foo')
188+
```
189+
190+
### Audience Claim
191+
192+
> The aud (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the aud claim when this claim is present, then the JWT MUST be rejected. In the general case, the aud value is an array of case-sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the aud value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL.
193+
194+
```python
195+
import jwt
196+
197+
198+
payload = {
199+
'some': 'payload',
200+
'aud': 'urn:foo'
201+
}
202+
203+
token = jwt.encode(payload, 'secret')
204+
decoded = jwt.decode(token, 'secret', audience='urn:foo')
205+
```
160206

161207
## License
162208

jwt/__init__.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ class ExpiredSignature(Exception):
3737
pass
3838

3939

40+
class InvalidAudience(Exception):
41+
pass
42+
43+
44+
class InvalidIssuer(Exception):
45+
pass
46+
47+
4048
signing_methods = {
4149
'none': lambda msg, key: b'',
4250
'HS256': lambda msg, key: hmac.new(key, msg, hashlib.sha256).digest(),
@@ -246,12 +254,15 @@ def encode(payload, key, algorithm='HS256', headers=None):
246254
return b'.'.join(segments)
247255

248256

249-
def decode(jwt, key='', verify=True, verify_expiration=True, leeway=0):
257+
def decode(jwt, key='', verify=True, **kwargs):
250258
payload, signing_input, header, signature = load(jwt)
251259

252260
if verify:
261+
verify_expiration = kwargs.pop('verify_expiration', True)
262+
leeway = kwargs.pop('leeway', 0)
263+
253264
verify_signature(payload, signing_input, header, signature, key,
254-
verify_expiration, leeway)
265+
verify_expiration, leeway, **kwargs)
255266

256267
return payload
257268

@@ -292,7 +303,7 @@ def load(jwt):
292303

293304

294305
def verify_signature(payload, signing_input, header, signature, key='',
295-
verify_expiration=True, leeway=0):
306+
verify_expiration=True, leeway=0, **kwargs):
296307
try:
297308
algorithm = header['alg'].upper()
298309
key = prepare_key_methods[algorithm](key)
@@ -310,6 +321,7 @@ def verify_signature(payload, signing_input, header, signature, key='',
310321

311322
if 'nbf' in payload and verify_expiration:
312323
utc_timestamp = timegm(datetime.utcnow().utctimetuple())
324+
313325
if payload['nbf'] > (utc_timestamp + leeway):
314326
raise ExpiredSignature('Signature not yet valid')
315327

@@ -318,3 +330,20 @@ def verify_signature(payload, signing_input, header, signature, key='',
318330

319331
if payload['exp'] < (utc_timestamp - leeway):
320332
raise ExpiredSignature('Signature has expired')
333+
334+
audience = kwargs.get('audience')
335+
336+
if audience:
337+
if isinstance(audience, list):
338+
audiences = audience
339+
else:
340+
audiences = [audience]
341+
342+
if payload.get('aud') not in audiences:
343+
raise InvalidAudience('Invalid audience')
344+
345+
issuer = kwargs.get('issuer')
346+
347+
if issuer:
348+
if payload.get('iss') != issuer:
349+
raise InvalidIssuer('Invalid issuer')

tests/test_jwt.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,126 @@ def test_ecdsa_related_key_preparation_methods(self):
691691
self.assertFalse('ES384' in jwt.prepare_key_methods)
692692
self.assertFalse('ES512' in jwt.prepare_key_methods)
693693

694+
def test_check_audience(self):
695+
audience = 'urn:foo'
696+
697+
payload = {
698+
'some': 'payload',
699+
'aud': 'urn:foo'
700+
}
701+
702+
token = jwt.encode(payload, 'secret')
703+
decoded = jwt.decode(token, 'secret', audience=audience)
704+
705+
self.assertEqual(decoded, payload)
706+
707+
def test_check_audience_in_array(self):
708+
audience = ['urn:foo', 'urn:other']
709+
710+
payload = {
711+
'some': 'payload',
712+
'aud': 'urn:foo'
713+
}
714+
715+
token = jwt.encode(payload, 'secret')
716+
decoded = jwt.decode(token, 'secret', audience=audience)
717+
718+
self.assertEqual(decoded, payload)
719+
720+
def test_raise_exception_invalid_audience(self):
721+
audience = 'urn:wrong'
722+
723+
payload = {
724+
'some': 'payload',
725+
'aud': 'urn:foo'
726+
}
727+
728+
token = jwt.encode(payload, 'secret')
729+
730+
self.assertRaises(
731+
jwt.InvalidAudience,
732+
lambda: jwt.decode(token, 'secret', audience=audience))
733+
734+
def test_raise_exception_invalid_audience_in_array(self):
735+
audience = ['urn:wrong', 'urn:morewrong']
736+
737+
payload = {
738+
'some': 'payload',
739+
'aud': 'urn:foo'
740+
}
741+
742+
token = jwt.encode(payload, 'secret')
743+
744+
self.assertRaises(
745+
jwt.InvalidAudience,
746+
lambda: jwt.decode(token, 'secret', audience=audience))
747+
748+
def test_raise_exception_token_without_audience(self):
749+
audience = 'urn:wrong'
750+
751+
payload = {
752+
'some': 'payload',
753+
}
754+
755+
token = jwt.encode(payload, 'secret')
756+
757+
self.assertRaises(
758+
jwt.InvalidAudience,
759+
lambda: jwt.decode(token, 'secret', audience=audience))
760+
761+
def test_raise_exception_token_without_audience_in_array(self):
762+
audience = ['urn:wrong', 'urn:morewrong']
763+
764+
payload = {
765+
'some': 'payload',
766+
}
767+
768+
token = jwt.encode(payload, 'secret')
769+
770+
self.assertRaises(
771+
jwt.InvalidAudience,
772+
lambda: jwt.decode(token, 'secret', audience=audience))
773+
774+
def test_check_issuer(self):
775+
issuer = 'urn:foo'
776+
777+
payload = {
778+
'some': 'payload',
779+
'iss': 'urn:foo'
780+
}
781+
782+
token = jwt.encode(payload, 'secret')
783+
decoded = jwt.decode(token, 'secret', issuer=issuer)
784+
785+
self.assertEqual(decoded, payload)
786+
787+
def test_raise_exception_invalid_issuer(self):
788+
issuer = 'urn:wrong'
789+
790+
payload = {
791+
'some': 'payload',
792+
'iss': 'urn:foo'
793+
}
794+
795+
token = jwt.encode(payload, 'secret')
796+
797+
self.assertRaises(
798+
jwt.InvalidIssuer,
799+
lambda: jwt.decode(token, 'secret', issuer=issuer))
800+
801+
def test_raise_exception_token_without_issuer(self):
802+
issuer = 'urn:wrong'
803+
804+
payload = {
805+
'some': 'payload',
806+
}
807+
808+
token = jwt.encode(payload, 'secret')
809+
810+
self.assertRaises(
811+
jwt.InvalidIssuer,
812+
lambda: jwt.decode(token, 'secret', issuer=issuer))
813+
694814

695815
if __name__ == '__main__':
696816
unittest.main()

0 commit comments

Comments
 (0)