Repository: GoogleCloudPlatform/google-auth-library-python
Pull request #87 (2016-11-16 22:52:11)

Fix ID token verification
* Fix call to `transport.Request`.
* Make jwt support base64 strings lacking padding characters.

jonparrott: @dhermes when replacing some hand-rolled code that verified firebase auth tokens for an app engine sample.

Patch:
diff --git a/google/auth/_helpers.py b/google/auth/_helpers.py
index 5a6c3a8..de86bee 100644
--- a/google/auth/_helpers.py
+++ b/google/auth/_helpers.py
@@ -14,6 +14,7 @@
 
 """Helper functions for commonly used utilities."""
 
+import base64
 import calendar
 import datetime
 
@@ -194,3 +195,19 @@ def string_to_scopes(scopes):
         return []
 
     return scopes.split(' ')
+
+
+def padded_urlsafe_b64decode(value):
+    """Decodes base64 strings lacking padding characters.
+
+    Google infrastructure tends to omit the base64 padding characters.
+
+    Args:
+        value (Union[str, bytes]): The encoded value.
+
+    Returns:
+        bytes: The decoded value
+    """
+    b64string = to_bytes(value)
+    padded = b64string + b'=' * (-len(b64string) % 4)
+    return base64.urlsafe_b64decode(padded)
diff --git a/google/auth/jwt.py b/google/auth/jwt.py
index 7abe4c2..0884b3d 100644
--- a/google/auth/jwt.py
+++ b/google/auth/jwt.py
@@ -96,7 +96,7 @@ def encode(signer, payload, header=None, key_id=None):
 
 def _decode_jwt_segment(encoded_section):
     """Decodes a single JWT segment."""
-    section_bytes = base64.urlsafe_b64decode(encoded_section)
+    section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section)
     try:
         return json.loads(section_bytes.decode('utf-8'))
     except ValueError:
@@ -124,7 +124,7 @@ def _unverified_decode(token):
 
     encoded_header, encoded_payload, signature = token.split(b'.')
     signed_section = encoded_header + b'.' + encoded_payload
-    signature = base64.urlsafe_b64decode(signature)
+    signature = _helpers.padded_urlsafe_b64decode(signature)
 
     # Parse segments
     header = _decode_jwt_segment(encoded_header)
diff --git a/google/oauth2/id_token.py b/google/oauth2/id_token.py
index 968f271..fa96fc0 100644
--- a/google/oauth2/id_token.py
+++ b/google/oauth2/id_token.py
@@ -47,7 +47,7 @@ def _fetch_certs(request, certs_url):
         Mapping[str, str]: A mapping of public key ID to x.509 certificate
             data.
     """
-    response = request('GET', certs_url)
+    response = request(certs_url, method='GET')
 
     if response.status != http_client.OK:
         raise exceptions.TransportError(


Test patch:
diff --git a/tests/oauth2/test_id_token.py b/tests/oauth2/test_id_token.py
index f3da663..7f89547 100644
--- a/tests/oauth2/test_id_token.py
+++ b/tests/oauth2/test_id_token.py
@@ -18,6 +18,7 @@ import mock
 import pytest
 
 from google.auth import exceptions
+import google.auth.transport
 from google.oauth2 import id_token
 
 
@@ -28,7 +29,7 @@ def make_request(status, data=None):
     if data is not None:
         response.data = json.dumps(data).encode('utf-8')
 
-    return mock.Mock(return_value=response)
+    return mock.Mock(return_value=response, spec=google.auth.transport.Request)
 
 
 def test__fetch_certs_success():
@@ -37,7 +38,7 @@ def test__fetch_certs_success():
 
     returned_certs = id_token._fetch_certs(request, mock.sentinel.cert_url)
 
-    request.assert_called_once_with('GET', mock.sentinel.cert_url)
+    request.assert_called_once_with(mock.sentinel.cert_url, method='GET')
     assert returned_certs == certs
 
 
@@ -47,7 +48,7 @@ def test__fetch_certs_failure():
     with pytest.raises(exceptions.TransportError):
         id_token._fetch_certs(request, mock.sentinel.cert_url)
 
-    request.assert_called_once_with('GET', mock.sentinel.cert_url)
+    request.assert_called_once_with(mock.sentinel.cert_url, method='GET')
 
 
 @mock.patch('google.auth.jwt.decode', autospec=True)
diff --git a/tests/test__helpers.py b/tests/test__helpers.py
index d413adc..b067faa 100644
--- a/tests/test__helpers.py
+++ b/tests/test__helpers.py
@@ -151,3 +151,19 @@ def test_string_to_scopes():
 
     for case, expected in cases:
         assert _helpers.string_to_scopes(case) == expected
+
+
+def test_padded_urlsafe_b64decode():
+    cases = [
+        ('YQ==', b'a'),
+        ('YQ', b'a'),
+        ('YWE=', b'aa'),
+        ('YWE', b'aa'),
+        ('YWFhYQ==', b'aaaa'),
+        ('YWFhYQ', b'aaaa'),
+        ('YWFhYWE=', b'aaaaa'),
+        ('YWFhYWE', b'aaaaa'),
+    ]
+
+    for case, expected in cases:
+        assert _helpers.padded_urlsafe_b64decode(case) == expected