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):