Repository: GoogleCloudPlatform/google-auth-library-python (python)

Issue:
Consider making `service_account_email` part of the `Signing` interface
google-cloud-python needs both a signer and a service account email to identify the signer to make a [signed url](https://github.com/GoogleCloudPlatform/google-cloud-python/blob/115748814bf6a59d99c13e070dfdcfbdec5e865d/core/google/cloud/credentials.py#L119).

/cc @dhermes 

Pull request:
Add Signing.singer_email
Resolves #85 

Patch:
diff --git a/google/auth/app_engine.py b/google/auth/app_engine.py
index 566475e..608651d 100644
--- a/google/auth/app_engine.py
+++ b/google/auth/app_engine.py
@@ -110,3 +110,8 @@ class Credentials(credentials.Scoped, credentials.Signing,
     @_helpers.copy_docstring(credentials.Signing)
     def sign_bytes(self, message):
         return app_identity.sign_blob(message)
+
+    @property
+    @_helpers.copy_docstring(credentials.Signing)
+    def signer_email(self):
+        return self.service_account_email
diff --git a/google/auth/credentials.py b/google/auth/credentials.py
index 470f1a4..360dc0e 100644
--- a/google/auth/credentials.py
+++ b/google/auth/credentials.py
@@ -229,3 +229,10 @@ class Signing(object):
         # pylint: disable=missing-raises-doc,redundant-returns-doc
         # (pylint doesn't recognize that this is abstract)
         raise NotImplementedError('Sign bytes must be implemented.')
+
+    @abc.abstractproperty
+    def signer_email(self):
+        """Optional[str]: An email address that identifies the signer."""
+        # pylint: disable=missing-raises-doc
+        # (pylint doesn't recognize that this is abstract)
+        raise NotImplementedError('Signer email must be implemented.')
diff --git a/google/auth/jwt.py b/google/auth/jwt.py
index 0884b3d..dfaf2e6 100644
--- a/google/auth/jwt.py
+++ b/google/auth/jwt.py
@@ -463,6 +463,11 @@ class Credentials(credentials.Signing,
         """
         return self._signer.sign(message)
 
+    @property
+    @_helpers.copy_docstring(credentials.Signing)
+    def signer_email(self):
+        return self._issuer
+
     def before_request(self, request, method, url, headers):
         """Performs credential-specific before request logic.
 
diff --git a/google/auth/transport/grpc.py b/google/auth/transport/grpc.py
index 1e02b38..4062972 100644
--- a/google/auth/transport/grpc.py
+++ b/google/auth/transport/grpc.py
@@ -62,7 +62,7 @@ class AuthMetadataPlugin(grpc.AuthMetadataPlugin):
 
 
 def secure_authorized_channel(
-        credentials, request, target, ssl_credentials=None):
+        credentials, request, target, ssl_credentials=None, **kwargs):
     """Creates a secure authorized gRPC channel.
 
     This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
@@ -98,6 +98,7 @@ def secure_authorized_channel(
         target (str): The host and port of the service.
         ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
             credentials. This can be used to specify different certificates.
+        kwargs: Additional arguments to pass to :func:`grpc.secure_channel`.
 
     Returns:
         grpc.Channel: The created gRPC channel.
@@ -115,4 +116,4 @@ def secure_authorized_channel(
     composite_credentials = grpc.composite_channel_credentials(
         ssl_credentials, google_auth_credentials)
 
-    return grpc.secure_channel(target, composite_credentials)
+    return grpc.secure_channel(target, composite_credentials, **kwargs)
diff --git a/google/oauth2/service_account.py b/google/oauth2/service_account.py
index 24c852b..48b537d 100644
--- a/google/oauth2/service_account.py
+++ b/google/oauth2/service_account.py
@@ -317,3 +317,8 @@ class Credentials(credentials.Signing,
     @_helpers.copy_docstring(credentials.Signing)
     def sign_bytes(self, message):
         return self._signer.sign(message)
+
+    @property
+    @_helpers.copy_docstring(credentials.Signing)
+    def signer_email(self):
+        return self._service_account_email


Test patch:
diff --git a/tests/oauth2/test_service_account.py b/tests/oauth2/test_service_account.py
index 678e6a3..e6ce631 100644
--- a/tests/oauth2/test_service_account.py
+++ b/tests/oauth2/test_service_account.py
@@ -134,6 +134,9 @@ class TestCredentials(object):
         signature = self.credentials.sign_bytes(to_sign)
         assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
 
+    def test_signer_email(self):
+        assert self.credentials.signer_email == self.SERVICE_ACCOUNT_EMAIL
+
     def test_create_scoped(self):
         scopes = ['email', 'profile']
         credentials = self.credentials.with_scopes(scopes)
diff --git a/tests/test_app_engine.py b/tests/test_app_engine.py
index 94f528d..117533e 100644
--- a/tests/test_app_engine.py
+++ b/tests/test_app_engine.py
@@ -115,3 +115,7 @@ class TestCredentials(object):
 
         assert signature == mock.sentinel.signature
         app_identity_mock.sign_blob.assert_called_with(to_sign)
+
+    def test_signer_email(self, app_identity_mock):
+        credentials = app_engine.Credentials()
+        assert credentials.signer_email == credentials.service_account_email
diff --git a/tests/test_jwt.py b/tests/test_jwt.py
index 2a4795a..3959260 100644
--- a/tests/test_jwt.py
+++ b/tests/test_jwt.py
@@ -264,6 +264,10 @@ class TestCredentials:
         signature = self.credentials.sign_bytes(to_sign)
         assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
 
+    def test_signer_email(self):
+        assert (self.credentials.signer_email ==
+                SERVICE_ACCOUNT_INFO['client_email'])
+
     def _verify_token(self, token):
         payload = jwt.decode(token, PUBLIC_CERT_BYTES)
         assert payload['iss'] == self.SERVICE_ACCOUNT_EMAIL
diff --git a/tests/transport/test_grpc.py b/tests/transport/test_grpc.py
index 84e4b56..15a301f 100644
--- a/tests/transport/test_grpc.py
+++ b/tests/transport/test_grpc.py
@@ -82,7 +82,7 @@ def test_secure_authorized_channel(
     target = 'example.com:80'
 
     channel = google.auth.transport.grpc.secure_authorized_channel(
-        credentials, request, target)
+        credentials, request, target, options=mock.sentinel.options)
 
     # Check the auth plugin construction.
     auth_plugin = metadata_call_credentials.call_args[0][0]
@@ -101,7 +101,8 @@ def test_secure_authorized_channel(
 
     # Check the channel call.
     secure_channel.assert_called_once_with(
-        target, composite_channel_credentials.return_value)
+        target, composite_channel_credentials.return_value,
+        options=mock.sentinel.options)
     assert channel == secure_channel.return_value