Add grpc transport (#67)

diff --git a/docs/conf.py b/docs/conf.py
index cfd452f..7b820b8 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -373,3 +373,4 @@
 # Autodoc config
 autoclass_content = 'both'
 autodoc_member_order = 'bysource'
+autodoc_mock_imports = ['grpc']
diff --git a/docs/reference/google.auth.transport.grpc.rst b/docs/reference/google.auth.transport.grpc.rst
new file mode 100644
index 0000000..e16d1d8
--- /dev/null
+++ b/docs/reference/google.auth.transport.grpc.rst
@@ -0,0 +1,7 @@
+google.auth.transport.grpc module
+=================================
+
+.. automodule:: google.auth.transport.grpc
+    :members:
+    :inherited-members:
+    :show-inheritance:
diff --git a/docs/reference/google.auth.transport.rst b/docs/reference/google.auth.transport.rst
index 7767daf..1f802c3 100644
--- a/docs/reference/google.auth.transport.rst
+++ b/docs/reference/google.auth.transport.rst
@@ -11,6 +11,7 @@
 
 .. toctree::
 
+   google.auth.transport.grpc
    google.auth.transport.requests
    google.auth.transport.urllib3
 
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
index 7af895f..aa96755 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -1,2 +1,3 @@
 sphinx-docstring-typing
 urllib3
+requests
diff --git a/google/auth/transport/grpc.py b/google/auth/transport/grpc.py
new file mode 100644
index 0000000..dfb2883
--- /dev/null
+++ b/google/auth/transport/grpc.py
@@ -0,0 +1,118 @@
+# 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.
+
+"""Authorization support for gRPC."""
+
+from __future__ import absolute_import
+
+import grpc
+
+
+class AuthMetadataPlugin(grpc.AuthMetadataPlugin):
+    """A `gRPC AuthMetadataPlugin`_ that inserts the credentials into each
+    request.
+
+    .. _gRPC AuthMetadataPlugin:
+        http://www.grpc.io/grpc/python/grpc.html#grpc.AuthMetadataPlugin
+
+    Args:
+        credentials (google.auth.credentials.Credentials): The credentials to
+            add to requests.
+        request (google.auth.transport.Request): A HTTP transport request
+            object used to refresh credentials as needed.
+    """
+    def __init__(self, credentials, request):
+        self._credentials = credentials
+        self._request = request
+
+    def _get_authorization_headers(self):
+        """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)
+
+        return [
+            ('authorization', 'Bearer {}'.format(self._credentials.token))
+        ]
+
+    def __call__(self, context, callback):
+        """Passes authorization metadata into the given callback.
+
+        Args:
+            context (grpc.AuthMetadataContext): The RPC context.
+            callback (grpc.AuthMetadataPluginCallback): The callback that will
+                be invoked to pass in the authorization metadata.
+        """
+        callback(self._get_authorization_headers(), None)
+
+
+def secure_authorized_channel(
+        credentials, target, request, ssl_credentials=None):
+    """Creates a secure authorized gRPC channel.
+
+    This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
+    channel can be used to create a stub that can make authorized requests.
+
+    Example::
+
+        import google.auth
+        import google.auth.transport.grpc
+        import google.auth.transport.requests
+        from google.cloud.speech.v1 import cloud_speech_pb2
+
+        # Get credentials.
+        credentials, _ = google.auth.default()
+
+        # Get an HTTP request function to refresh credentials.
+        request = google.auth.transport.requests.Request()
+
+        # Create a channel.
+        channel = google.auth.transport.grpc.secure_authorized_channel(
+            credentials, 'speech.googleapis.com:443', request)
+
+        # Use the channel to create a stub.
+        cloud_speech.create_Speech_stub(channel)
+
+    Args:
+        credentials (google.auth.credentials.Credentials): The credentials to
+            add to requests.
+        target (str): The host and port of the service.
+        request (google.auth.transport.Request): A HTTP transport request
+            object used to refresh credentials as needed. Even though gRPC
+            is a separate transport, there's no way to refresh the credentials
+            without using a standard http transport.
+        ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
+            credentials. This can be used to specify different certificates.
+
+    Returns:
+        grpc.Channel: The created gRPC channel.
+    """
+    # Create the metadata plugin for inserting the authorization header.
+    metadata_plugin = AuthMetadataPlugin(credentials, request)
+
+    # Create a set of grpc.CallCredentials using the metadata plugin.
+    google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
+
+    if ssl_credentials is None:
+        ssl_credentials = grpc.ssl_channel_credentials()
+
+    # Combine the ssl credentials and the authorization credentials.
+    composite_credentials = grpc.composite_channel_credentials(
+        ssl_credentials, google_auth_credentials)
+
+    return grpc.secure_channel(target, composite_credentials)
diff --git a/scripts/run_pylint.py b/scripts/run_pylint.py
index ed994ff..b06a98c 100644
--- a/scripts/run_pylint.py
+++ b/scripts/run_pylint.py
@@ -73,11 +73,12 @@
     'no-self-use',
     'redefined-outer-name',
     'unused-argument',
+    'no-name-in-module',
 ])
 _TEST_RC_REPLACEMENTS = copy.deepcopy(_PRODUCTION_RC_REPLACEMENTS)
 _TEST_RC_REPLACEMENTS.setdefault('BASIC', {})
 _TEST_RC_REPLACEMENTS['BASIC'].update({
-    'good-names': ['i', 'j', 'k', 'ex', 'Run', '_', 'fh'],
+    'good-names': ['i', 'j', 'k', 'ex', 'Run', '_', 'fh', 'pytestmark'],
     'method-rgx': '[a-z_][a-z0-9_]{2,80}$',
     'function-rgx': '[a-z_][a-z0-9_]{2,80}$',
 })
diff --git a/system_tests/nox.py b/system_tests/nox.py
index 6429f7b..5df72c8 100644
--- a/system_tests/nox.py
+++ b/system_tests/nox.py
@@ -248,3 +248,9 @@
     session.env['TEST_APP_URL'] = application_url
     session.chdir(HERE)
     session.run('pytest', 'test_app_engine.py')
+
+
+def session_grpc(session):
+    session.virtualenv = False
+    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+    session.run('pytest', 'test_grpc.py')
diff --git a/system_tests/test_grpc.py b/system_tests/test_grpc.py
new file mode 100644
index 0000000..0db9f1d
--- /dev/null
+++ b/system_tests/test_grpc.py
@@ -0,0 +1,40 @@
+# 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.
+
+import google.auth
+import google.auth.credentials
+import google.auth.transport.grpc
+from google.cloud.gapic.pubsub.v1 import publisher_api
+
+
+def test_grpc_request(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, target, http_request)
+
+    # Create a pub/sub client.
+    client = publisher_api.PublisherApi(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)
diff --git a/tests/transport/test_grpc.py b/tests/transport/test_grpc.py
new file mode 100644
index 0000000..2b214a7
--- /dev/null
+++ b/tests/transport/test_grpc.py
@@ -0,0 +1,129 @@
+# 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.
+
+import mock
+
+import pytest
+
+try:
+    import google.auth.transport.grpc
+    HAS_GRPC = True
+except ImportError:  # pragma: NO COVER
+    HAS_GRPC = False
+
+
+pytestmark = pytest.mark.skipif(not HAS_GRPC, reason='gRPC is unavailable.')
+
+
+class MockCredentials(object):
+    def __init__(self, token='token'):
+        self.token = token
+        self.valid = True
+        self.expired = False
+
+    def refresh(self, request):
+        self.token += '1'
+
+
+class TestAuthMetadataPlugin(object):
+    def test_call_no_refresh(self):
+        credentials = MockCredentials()
+        request = mock.Mock()
+
+        plugin = google.auth.transport.grpc.AuthMetadataPlugin(
+            credentials, request)
+
+        context = mock.Mock()
+        callback = mock.Mock()
+
+        plugin(context, callback)
+
+        assert callback.called_once_with(
+            [('authorization', 'Bearer {}'.format(credentials.token))], None)
+
+    def test_call_refresh(self):
+        credentials = MockCredentials()
+        credentials.expired = True
+        request = mock.Mock()
+
+        plugin = google.auth.transport.grpc.AuthMetadataPlugin(
+            credentials, request)
+
+        context = mock.Mock()
+        callback = mock.Mock()
+
+        plugin(context, callback)
+
+        assert credentials.token == 'token1'
+        assert callback.called_once_with(
+            [('authorization', 'Bearer {}'.format(credentials.token))], None)
+
+
+@mock.patch('grpc.composite_channel_credentials')
+@mock.patch('grpc.metadata_call_credentials')
+@mock.patch('grpc.ssl_channel_credentials')
+@mock.patch('grpc.secure_channel')
+def test_secure_authorized_channel(
+        secure_channel, ssl_channel_credentials, metadata_call_credentials,
+        composite_channel_credentials):
+    credentials = mock.Mock()
+    request = mock.Mock()
+    target = 'example.com:80'
+
+    channel = google.auth.transport.grpc.secure_authorized_channel(
+        credentials, target, request)
+
+    # Check the auth plugin construction.
+    auth_plugin = metadata_call_credentials.call_args[0][0]
+    assert isinstance(
+        auth_plugin, google.auth.transport.grpc.AuthMetadataPlugin)
+    assert auth_plugin._credentials == credentials
+    assert auth_plugin._request == request
+
+    # Check the ssl channel call.
+    assert ssl_channel_credentials.called
+
+    # Check the composite credentials call.
+    composite_channel_credentials.assert_called_once_with(
+        ssl_channel_credentials.return_value,
+        metadata_call_credentials.return_value)
+
+    # Check the channel call.
+    secure_channel.assert_called_once_with(
+        target, composite_channel_credentials.return_value)
+    assert channel == secure_channel.return_value
+
+
+@mock.patch('grpc.composite_channel_credentials')
+@mock.patch('grpc.metadata_call_credentials')
+@mock.patch('grpc.ssl_channel_credentials')
+@mock.patch('grpc.secure_channel')
+def test_secure_authorized_channel_explicit_ssl(
+        secure_channel, ssl_channel_credentials, metadata_call_credentials,
+        composite_channel_credentials):
+    credentials = mock.Mock()
+    request = mock.Mock()
+    target = 'example.com:80'
+    ssl_credentials = mock.Mock()
+
+    google.auth.transport.grpc.secure_authorized_channel(
+        credentials, target, request, ssl_credentials=ssl_credentials)
+
+    # Check the ssl channel call.
+    assert not ssl_channel_credentials.called
+
+    # Check the composite credentials call.
+    composite_channel_credentials.assert_called_once_with(
+        ssl_credentials,
+        metadata_call_credentials.return_value)
diff --git a/tox.ini b/tox.ini
index f41ffd1..c77487c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,6 +11,7 @@
   urllib3
   certifi
   requests
+  grpcio; platform_python_implementation != 'PyPy'
 commands =
   py.test --cov=google.auth --cov=google.oauth2 --cov=tests {posargs:tests}
 
@@ -30,6 +31,7 @@
 deps =
   {[testenv]deps}
   nox-automation
+  gapic-google-pubsub-v1==0.11.1
 passenv =
   SKIP_APP_ENGINE_SYSTEM_TEST
   CLOUD_SDK_ROOT
@@ -42,6 +44,7 @@
 deps =
   {[testenv]deps}
   nox-automation
+  gapic-google-pubsub-v1==0.11.1
 passenv =
   SKIP_APP_ENGINE_SYSTEM_TEST
   CLOUD_SDK_ROOT