Skip to content

Commit

Permalink
Add verify_at_hash option (default=False)
Browse files Browse the repository at this point in the history
To make it easier to enforce verification of at_hash in future major
versions of PyJWT, add the verify_at_hash option to the implementation,
defaulting to False. It's now necessary for callers to set
`options={'verify_at_hash': True}` in addition to `access_token=...`,
but it keeps backwards compatibility for users who are acting on OIDC ID
Tokens with current versions of PyJWT.
Whenever PyJWT 2.0 is created, this can be changed to default to True
  • Loading branch information
sirosen committed Oct 19, 2017
1 parent fb399e3 commit 5709301
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 7 deletions.
17 changes: 12 additions & 5 deletions jwt/api_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def _get_default_options():
'verify_iat': True,
'verify_aud': True,
'verify_iss': True,
# TODO: in v2.0, make this default to True
'verify_at_hash': False,
'require_exp': False,
'require_iat': False,
'require_nbf': False
Expand Down Expand Up @@ -173,7 +175,7 @@ def _validate_claims(self, payload, header, options, audience=None,
if 'exp' in payload and options.get('verify_exp'):
self._validate_exp(payload, now, leeway)

if access_token:
if options.get('verify_at_hash'):
self._validate_at_hash(payload, header, access_token)

if options.get('verify_iss'):
Expand Down Expand Up @@ -248,11 +250,16 @@ def _validate_iss(self, payload, issuer):
raise InvalidIssuerError('Invalid issuer')

def _validate_at_hash(self, payload, header, access_token):
try:
at_hash = payload['at_hash']
except KeyError:
raise MissingRequiredClaimError('at_hash')
if 'at_hash' not in payload:
if access_token is None:
return
else:
raise MissingRequiredClaimError('at_hash')
elif access_token is None:
raise InvalidAccessTokenHashError(
"access_token=None can't be hashed")

at_hash = payload['at_hash']
alg = header.get('alg')

if at_hash != self.compute_at_hash(access_token, alg):
Expand Down
37 changes: 35 additions & 2 deletions tests/test_api_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,31 @@ def test_load_verify_valid_jwt(self, jwt):

assert decoded_payload == example_payload

def test_verify_athash_with_no_access_token(self, jwt, payload):
"""
Checks that the default (False) on verify_at_hash can be changed safely
for tokens which don't include at_hash
There's no at_hash here, but we're asking for verification anyway,
while not passing an access_token
It's known to be a breaking change to set it to True for tokens which
include at_hash
"""
secret = 'secret'
jwt_message = jwt.encode(payload, secret)

decoded_payload = jwt.decode(jwt_message, key=secret,
options={'verify_at_hash': True})

assert decoded_payload == payload

def test_verify_fails_missing_athash(self, jwt, payload):
secret = 'secret'
jwt_message = jwt.encode(payload, secret)

with pytest.raises(MissingRequiredClaimError) as exc:
jwt.decode(jwt_message, key=secret, access_token='foobar')
jwt.decode(jwt_message, key=secret, access_token='foobar',
options={'verify_at_hash': True})
assert 'at_hash' in str(exc.value)

def test_decode_invalid_payload_string(self, jwt):
Expand Down Expand Up @@ -131,14 +150,28 @@ def test_decode_with_wrong_access_token_throws_exception(self, jwt, payload):
jwt_message = jwt.encode(payload, secret, access_token='foobar')

with pytest.raises(InvalidAccessTokenHashError):
jwt.decode(jwt_message, key=secret, access_token='foobar2')
jwt.decode(jwt_message, key=secret, access_token='foobar2',
options={'verify_at_hash': True})

def test_decode_with_no_access_token_skips_at_hash(self, jwt, payload):
"""
This is a backwards-compatibility test to ensure that decoding in
ignorance of the at_hash claim won't be broken by the addition of
at_hash verification
"""
secret = 'secret'
jwt_message = jwt.encode(payload, secret, access_token='foobar')

jwt.decode(jwt_message, key=secret)

def test_decode_with_no_access_token_fails(self, jwt, payload):
secret = 'secret'
jwt_message = jwt.encode(payload, secret, access_token='foobar')

with pytest.raises(InvalidAccessTokenHashError):
jwt.decode(jwt_message, key=secret,
options={'verify_at_hash': True})

def test_encode_bad_type(self, jwt):

types = ['string', tuple(), list(), 42, set()]
Expand Down

0 comments on commit 5709301

Please sign in to comment.