Repository: mpdavis/python-jose

Issue:
Should at_hash claim verification fail when missing from JWT?
It looks like `at_hash` in JWT payload is optional (see http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken).

However, in python-jose, when both `id_token` and `access_token` parameters are specified, decoding a JWT that has no `at_hash` claim raises an error (*at_hash claim missing from token*)
https://github.com/mpdavis/python-jose/pull/30/files#diff-b106d01229785c64375df96ca4b3f58cR422

Shouldn't it be acceptable since the spec says it's optional?

Obviously we can disable at_hash verification with the appropriate decode option, but we find it useful to perform claims verification on JWT that have it or not with the same code. Maybe with a `allow_missing_at_hash` option or something?

Huge thanks for this lib 😻 

Discussion:
mpdavis: It looks like there are builds failing on Python 2.6 and 3.3.

Python 2.6 is failing because pytest dropped support. It looks like we will need to pin pytest in `tox.ini` for 2.6 builds (or possibly just all builds if easier).

I am still looking into the 3.3 failure. I can take a look later if you don't want to worry about it. 
leplatrem: I also saw this:
```
$ tox

Matching undeclared envs is deprecated. Be sure all the envs that Tox should run are declared in the tox config.
```

Patch:
diff --git a/jose/jwt.py b/jose/jwt.py
index 2da511f..3ba3250 100644
--- a/jose/jwt.py
+++ b/jose/jwt.py
@@ -408,24 +408,28 @@ def _validate_jti(claims):
 
 def _validate_at_hash(claims, access_token, algorithm):
     """
-    Validates that the 'at_hash' parameter included in the claims matches
-    with the access_token returned alongside the id token as part of
-    the authorization_code flow.
+    Validates that the 'at_hash' is valid.
+
+    Its value is the base64url encoding of the left-most half of the hash
+    of the octets of the ASCII representation of the access_token value,
+    where the hash algorithm used is the hash algorithm used in the alg
+    Header Parameter of the ID Token's JOSE Header. For instance, if the
+    alg is RS256, hash the access_token value with SHA-256, then take the
+    left-most 128 bits and base64url encode them. The at_hash value is a
+    case sensitive string.  Use of this claim is OPTIONAL.
 
     Args:
-        claims (dict): The claims dictionary to validate.
-        access_token (str): The access token returned by the OpenID Provider.
-        algorithm (str): The algorithm used to sign the JWT, as specified by
-            the token headers.
+      claims (dict): The claims dictionary to validate.
+      access_token (str): The access token returned by the OpenID Provider.
+      algorithm (str): The algorithm used to sign the JWT, as specified by
+          the token headers.
     """
-    if 'at_hash' not in claims and not access_token:
+    if 'at_hash' not in claims:
         return
-    elif 'at_hash' in claims and not access_token:
+
+    if not access_token:
         msg = 'No access_token provided to compare against at_hash claim.'
         raise JWTClaimsError(msg)
-    elif access_token and 'at_hash' not in claims:
-        msg = 'at_hash claim missing from token.'
-        raise JWTClaimsError(msg)
 
     try:
         expected_hash = calculate_at_hash(access_token,
@@ -433,7 +437,7 @@ def _validate_at_hash(claims, access_token, algorithm):
     except (TypeError, ValueError):
         msg = 'Unable to calculate at_hash to verify against token claims.'
         raise JWTClaimsError(msg)
-        
+
     if claims['at_hash'] != expected_hash:
         raise JWTClaimsError('at_hash claim does not match access_token.')
 


Test patch:
diff --git a/tests/test_jwt.py b/tests/test_jwt.py
index 485fff5..beb6789 100644
--- a/tests/test_jwt.py
+++ b/tests/test_jwt.py
@@ -468,8 +468,8 @@ class TestJWT:
 
     def test_at_hash_missing_claim(self, claims, key):
         token = jwt.encode(claims, key)
-        with pytest.raises(JWTError):
-            jwt.decode(token, key, access_token='<ACCESS_TOKEN>')
+        payload = jwt.decode(token, key, access_token='<ACCESS_TOKEN>')
+        assert 'at_hash' not in payload
 
     def test_at_hash_unable_to_calculate(self, claims, key):
         token = jwt.encode(claims, key, access_token='<ACCESS_TOKEN>')