Consolidate service account file loading logic. (#31)

diff --git a/google/auth/_service_account_info.py b/google/auth/_service_account_info.py
new file mode 100644
index 0000000..b007abd
--- /dev/null
+++ b/google/auth/_service_account_info.py
@@ -0,0 +1,77 @@
+# Copyright 2016 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helper functions for loading data from a Google service account file."""
+
+import io
+import json
+
+import six
+
+from google.auth import crypt
+
+
+def from_dict(data, require=None):
+    """Validates a dictionary containing Google service account data.
+
+    Creates and returns a :class:`google.auth.crypt.Signer` instance from the
+    private key specified in the data.
+
+    Args:
+        data (Mapping[str, str]): The service account data
+        require (Sequence[str]): List of keys required to be present in the
+            info.
+
+    Returns:
+        google.auth.crypt.Signer: A signer created from the private key in the
+            service account file.
+
+    Raises:
+        ValueError: if the data was in the wrong format, or if one of the
+            required keys is missing.
+    """
+    # Private key is always required.
+    keys_needed = set(('private_key',))
+    if require is not None:
+        keys_needed.update(require)
+
+    missing = keys_needed.difference(six.iterkeys(data))
+
+    if missing:
+        raise ValueError(
+            'Service account info was not in the expected format, missing '
+            'fields {}.'.format(', '.join(missing)))
+
+    # Create a signer.
+    signer = crypt.Signer.from_string(
+        data['private_key'], data.get('private_key_id'))
+
+    return signer
+
+
+def from_filename(filename, require=None):
+    """Reads a Google service account JSON file and returns its parsed info.
+
+    Args:
+        filename (str): The path to the service account .json file.
+        require (Sequence[str]): List of keys required to be present in the
+            info.
+
+    Returns:
+        Tuple[ Mapping[str, str], google.auth.crypt.Signer ]: The verified
+            info and a signer instance.
+    """
+    with io.open(filename, 'r', encoding='utf-8') as json_file:
+        data = json.load(json_file)
+        return data, from_dict(data, require=require)
diff --git a/google/auth/jwt.py b/google/auth/jwt.py
index 5db1fa2..69575a1 100644
--- a/google/auth/jwt.py
+++ b/google/auth/jwt.py
@@ -43,12 +43,12 @@
 import base64
 import collections
 import datetime
-import io
 import json
 
 from six.moves import urllib
 
 from google.auth import _helpers
+from google.auth import _service_account_info
 from google.auth import credentials
 from google.auth import crypt
 
@@ -318,8 +318,28 @@
             self._additional_claims = {}
 
     @classmethod
+    def _from_signer_and_info(cls, signer, info, **kwargs):
+        """Creates a Credentials instance from a signer and service account
+        info.
+
+        Args:
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+            info (Mapping[str, str]): The service account info.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.Credentials: The constructed credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        kwargs.setdefault('subject', info['client_email'])
+        return cls(signer, issuer=info['client_email'], **kwargs)
+
+    @classmethod
     def from_service_account_info(cls, info, **kwargs):
-        """Creates a Credentials instance from parsed service account info.
+        """Creates a Credentials instance from a dictionary containing service
+        account info in Google format.
 
         Args:
             info (Mapping[str, str]): The service account info in Google
@@ -332,34 +352,25 @@
         Raises:
             ValueError: If the info is not in the expected format.
         """
-
-        try:
-            email = info['client_email']
-            key_id = info['private_key_id']
-            private_key = info['private_key']
-        except KeyError:
-            raise ValueError(
-                'Service account info was not in the expected format.')
-
-        signer = crypt.Signer.from_string(private_key, key_id)
-
-        kwargs.setdefault('subject', email)
-        return cls(signer, issuer=email, **kwargs)
+        signer = _service_account_info.from_dict(
+            info, require=['client_email'])
+        return cls._from_signer_and_info(signer, info, **kwargs)
 
     @classmethod
     def from_service_account_file(cls, filename, **kwargs):
-        """Creates a Credentials instance from a service account json file.
+        """Creates a Credentials instance from a service account .json file
+        in Google format.
 
         Args:
-            filename (str): The path to the service account json file.
+            filename (str): The path to the service account .json file.
             kwargs: Additional arguments to pass to the constructor.
 
         Returns:
             google.auth.jwt.Credentials: The constructed credentials.
         """
-        with io.open(filename, 'r', encoding='utf-8') as json_file:
-            info = json.load(json_file)
-        return cls.from_service_account_info(info, **kwargs)
+        info, signer = _service_account_info.from_filename(
+            filename, require=['client_email'])
+        return cls._from_signer_and_info(signer, info, **kwargs)
 
     def with_claims(self, issuer=None, subject=None, audience=None,
                     additional_claims=None):
diff --git a/google/oauth2/service_account.py b/google/oauth2/service_account.py
index 4c4c1c0..dfbe352 100644
--- a/google/oauth2/service_account.py
+++ b/google/oauth2/service_account.py
@@ -71,12 +71,10 @@
 """
 
 import datetime
-import io
-import json
 
 from google.auth import _helpers
+from google.auth import _service_account_info
 from google.auth import credentials
-from google.auth import crypt
 from google.auth import jwt
 from google.oauth2 import _client
 
@@ -150,6 +148,27 @@
             self._additional_claims = {}
 
     @classmethod
+    def _from_signer_and_info(cls, signer, info, **kwargs):
+        """Creates a Credentials instance from a signer and service account
+        info.
+
+        Args:
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+            info (Mapping[str, str]): The service account info.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.Credentials: The constructed credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        return cls(
+            signer,
+            service_account_email=info['client_email'],
+            token_uri=info['token_uri'], **kwargs)
+
+    @classmethod
     def from_service_account_info(cls, info, **kwargs):
         """Creates a Credentials instance from parsed service account info.
 
@@ -165,19 +184,9 @@
         Raises:
             ValueError: If the info is not in the expected format.
         """
-        try:
-            email = info['client_email']
-            key_id = info['private_key_id']
-            private_key = info['private_key']
-            token_uri = info['token_uri']
-        except KeyError:
-            raise ValueError(
-                'Service account info was not in the expected format.')
-
-        signer = crypt.Signer.from_string(private_key, key_id)
-
-        return cls(
-            signer, service_account_email=email, token_uri=token_uri, **kwargs)
+        signer = _service_account_info.from_dict(
+            info, require=['client_email', 'token_uri'])
+        return cls._from_signer_and_info(signer, info, **kwargs)
 
     @classmethod
     def from_service_account_file(cls, filename, **kwargs):
@@ -191,9 +200,9 @@
             google.auth.service_account.Credentials: The constructed
                 credentials.
         """
-        with io.open(filename, 'r', encoding='utf-8') as json_file:
-            info = json.load(json_file)
-        return cls.from_service_account_info(info, **kwargs)
+        info, signer = _service_account_info.from_filename(
+            filename, require=['client_email', 'token_uri'])
+        return cls._from_signer_and_info(signer, info, **kwargs)
 
     @property
     def requires_scopes(self):