Repository: googleapis/google-auth-library-python

Issue:
Determine how to handle before_request for gRPC
I discovered that I did something totally wrong when I implemented our grpc transport.

All of our HTTP transports call `credentials.before_request` to get the authorization header populated ([here](https://github.com/GoogleCloudPlatform/google-auth-library-python/blob/8e1dc4f0345bbcfbe0f43ce40a5f9152b56db652/google/auth/transport/requests.py#L167)). However, the gRPC transport instead calls `credentials.refresh` directly and constructs the header itself ([here](https://github.com/GoogleCloudPlatform/google-auth-library-python/blob/8e1dc4f0345bbcfbe0f43ce40a5f9152b56db652/google/auth/transport/grpc.py#L50)).

I think the gRPC transport needs to do the same as the others, but that's where it gets tricky. `before_request` takes 4 arguments `request, method, url, request_headers`. gRPC provides our auth plugin with a context that looks like `AuthMetadataContext(service_url='https://pubsub.googleapis.com/google.pubsub.v1.Publisher', method_name='ListTopics')`. Does it make sense to call `before_request(http_request, context.method_name, context.service_url, headers)` even though `context.method_name` is not an HTTP method? It it okay to overload the term `method` for different transports?

@dhermes @lukesneeringer thoughts?


Discussion:
I never understood why we cared that there was a gRPC "transport" since it can't do much/any of the handshakes over the wire. So in that light, why do we care about giving it a novel interface?
@dhermes I'm not sure what you mean. What's the alternative?
Just a module with gRPC helpers (which is essentially what it is already).
What would you remove from the current module? Or do you just not like that it's in `google.auth.transports`. 

At any rate, gRPC needs to support all of our existing credentials and needs to interact with them properly, so the gRPC auth metadata plugin needs to call `before_request` just like everything else.

Resolving patch:
diff --git a/google/auth/credentials.py b/google/auth/credentials.py
--- a/google/auth/credentials.py
+++ b/google/auth/credentials.py
@@ -104,8 +104,9 @@ def before_request(self, request, method, url, headers):
         Args:
             request (google.auth.transport.Request): The object used to make
                 HTTP requests.
-            method (str): The request's HTTP method.
-            url (str): The request's URI.
+            method (str): The request's HTTP method or the RPC method being
+                invoked.
+            url (str): The request's URI or the RPC service's URI.
             headers (Mapping): The request's headers.
         """
         # pylint: disable=unused-argument
diff --git a/google/auth/transport/grpc.py b/google/auth/transport/grpc.py
--- a/google/auth/transport/grpc.py
+++ b/google/auth/transport/grpc.py
@@ -17,6 +17,7 @@
 from __future__ import absolute_import
 
 import grpc
+import six
 
 
 class AuthMetadataPlugin(grpc.AuthMetadataPlugin):
@@ -40,19 +41,21 @@ def __init__(self, credentials, request):
         self._credentials = credentials
         self._request = request
 
-    def _get_authorization_headers(self):
+    def _get_authorization_headers(self, context):
         """Gets the authorization headers for a request.
 
         Returns:
             Sequence[Tuple[str, str]]: A list of request headers (key, value)
                 to add to the request.
         """
-        if self._credentials.expired or not self._credentials.valid:
-            self._credentials.refresh(self._request)
+        headers = {}
+        self._credentials.before_request(
+            self._request,
+            context.method_name,
+            context.service_url,
+            headers)
 
-        return [
-            ('authorization', 'Bearer {}'.format(self._credentials.token))
-        ]
+        return list(six.iteritems(headers))
 
     def __call__(self, context, callback):
         """Passes authorization metadata into the given callback.
@@ -62,7 +65,7 @@ def __call__(self, context, callback):
             callback (grpc.AuthMetadataPluginCallback): The callback that will
                 be invoked to pass in the authorization metadata.
         """
-        callback(self._get_authorization_headers(), None)
+        callback(self._get_authorization_headers(context), None)
 
 
 def secure_authorized_channel(


Test patch:
diff --git a/system_tests/test_grpc.py b/system_tests/test_grpc.py
--- a/system_tests/test_grpc.py
+++ b/system_tests/test_grpc.py
@@ -15,23 +15,40 @@
 import google.auth
 import google.auth.credentials
 import google.auth.transport.grpc
-from google.cloud.gapic.pubsub.v1 import publisher_api
+from google.cloud.gapic.pubsub.v1 import publisher_client
 
 
-def test_grpc_request(http_request):
+def test_grpc_request_with_regular_credentials(http_request):
     credentials, project_id = google.auth.default()
     credentials = google.auth.credentials.with_scopes_if_required(
         credentials, ['https://www.googleapis.com/auth/pubsub'])
 
-    target = '{}:{}'.format(
-        publisher_api.PublisherApi.SERVICE_ADDRESS,
-        publisher_api.PublisherApi.DEFAULT_SERVICE_PORT)
+    channel = google.auth.transport.grpc.secure_authorized_channel(
+        credentials,
+        http_request,
+        publisher_client.PublisherClient.SERVICE_ADDRESS)
+
+    # Create a pub/sub client.
+    client = publisher_client.PublisherClient(channel=channel)
+
+    # list the topics and drain the iterator to test that an authorized API
+    # call works.
+    list_topics_iter = client.list_topics(
+        project='projects/{}'.format(project_id))
+    list(list_topics_iter)
+
+
+def test_grpc_request_with_jwt_credentials(http_request):
+    credentials, project_id = google.auth.default()
+    credentials = credentials.to_jwt_credentials()
 
     channel = google.auth.transport.grpc.secure_authorized_channel(
-        credentials, http_request, target)
+        credentials,
+        http_request,
+        publisher_client.PublisherClient.SERVICE_ADDRESS)
 
     # Create a pub/sub client.
-    client = publisher_api.PublisherApi(channel=channel)
+    client = publisher_client.PublisherClient(channel=channel)
 
     # list the topics and drain the iterator to test that an authorized API
     # call works.
diff --git a/tests/transport/test_grpc.py b/tests/transport/test_grpc.py
--- a/tests/transport/test_grpc.py
+++ b/tests/transport/test_grpc.py
@@ -12,10 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import mock
+import datetime
 
+import mock
 import pytest
 
+from google.auth import credentials
 try:
     import google.auth.transport.grpc
     HAS_GRPC = True
@@ -26,11 +28,11 @@
 pytestmark = pytest.mark.skipif(not HAS_GRPC, reason='gRPC is unavailable.')
 
 
-class MockCredentials(object):
+class MockCredentials(credentials.Credentials):
     def __init__(self, token='token'):
+        super(MockCredentials, self).__init__()
         self.token = token
-        self.valid = True
-        self.expired = False
+        self.expiry = None
 
     def refresh(self, request):
         self.token += '1'
@@ -54,7 +56,7 @@ def test_call_no_refresh(self):
 
     def test_call_refresh(self):
         credentials = MockCredentials()
-        credentials.expired = True
+        credentials.expiry = datetime.datetime.min
         request = mock.Mock()
 
         plugin = google.auth.transport.grpc.AuthMetadataPlugin(