Merge pull request #19456 from lidizheng/example-auth

[Python] Add authentication extension example
pull/19485/head
Lidi Zheng 6 years ago committed by GitHub
commit 25c720f650
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 65
      examples/python/auth/BUILD.bazel
  2. 112
      examples/python/auth/README.md
  3. 31
      examples/python/auth/_credentials.py
  4. 19
      examples/python/auth/credentials/localhost.crt
  5. 27
      examples/python/auth/credentials/localhost.key
  6. 20
      examples/python/auth/credentials/root.crt
  7. 105
      examples/python/auth/customized_auth_client.py
  8. 110
      examples/python/auth/customized_auth_server.py
  9. 56
      examples/python/auth/test/_auth_example_test.py

@ -0,0 +1,65 @@
# Copyright 2019 The gRPC Authors
#
# 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.
filegroup(
name = "_credentials_files",
testonly = 1,
srcs = [
"credentials/localhost.key",
"credentials/localhost.crt",
"credentials/root.crt",
],
)
py_library(
name = "_credentials",
testonly = 1,
srcs = ["_credentials.py"],
data = [":_credentials_files"],
)
py_binary(
name = "customized_auth_client",
testonly = 1,
srcs = ["customized_auth_client.py"],
deps = [
":_credentials",
"//src/python/grpcio/grpc:grpcio",
"//examples:py_helloworld",
],
)
py_binary(
name = "customized_auth_server",
testonly = 1,
srcs = ["customized_auth_server.py"],
deps = [
":_credentials",
"//src/python/grpcio/grpc:grpcio",
"//examples:py_helloworld",
],
)
py_test(
name = "_auth_example_test",
srcs = ["test/_auth_example_test.py"],
deps = [
"//src/python/grpcio/grpc:grpcio",
"//examples:py_helloworld",
":customized_auth_client",
":customized_auth_server",
":_credentials",
],
)

@ -0,0 +1,112 @@
# Authentication Extension Example in gRPC Python
## Check Our Guide First
For most common usage of authentication in gRPC Python, please see our
[Authentication](https://grpc.io/docs/guides/auth/) guide's Python section. The
Guide includes following scenarios:
1. Server SSL credential setup
2. Client SSL credential setup
3. Authenticate with Google using a JWT
4. Authenticate with Google using an Oauth2 token
Also, the guide talks about gRPC specific credential types.
### Channel credentials
Channel credentials are attached to a `Channel` object, the most common use case
are SSL credentials.
### Call credentials
Call credentials are attached to a `Call` object (corresponding to an RPC).
Under the hood, the call credentials is a function that takes in information of
the RPC and modify metadata through callback.
## About This Example
This example focuses on extending gRPC authentication mechanism:
1) Customize authentication plugin;
2) Composite client side credentials;
3) Validation through interceptor on server side.
## AuthMetadataPlugin: Manipulate metadata for each call
Unlike TLS/SSL based authentication, the authentication extension in gRPC Python
lives at a much higher level of networking. It relies on the transmission of
metadata (HTTP Header) between client and server, instead of alternating the
transport protocol.
gRPC Python provides a way to intercept an RPC and append authentication related
metadata through
[`AuthMetadataPlugin`](https://grpc.github.io/grpc/python/grpc.html#grpc.AuthMetadataPlugin).
Those in need of a custom authentication method may simply provide a concrete
implementation of the following interface:
```Python
class AuthMetadataPlugin:
"""A specification for custom authentication."""
def __call__(self, context, callback):
"""Implements authentication by passing metadata to a callback.
Implementations of this method must not block.
Args:
context: An AuthMetadataContext providing information on the RPC that
the plugin is being called to authenticate.
callback: An AuthMetadataPluginCallback to be invoked either
synchronously or asynchronously.
"""
```
Then pass the instance of the concrete implementation to
`grpc.metadata_call_credentials` function to be converted into a
`CallCredentials` object. Please NOTE that it is possible to pass a Python
function object directly, but we recommend to inherit from the base class to
ensure implementation correctness.
```Python
def metadata_call_credentials(metadata_plugin, name=None):
"""Construct CallCredentials from an AuthMetadataPlugin.
Args:
metadata_plugin: An AuthMetadataPlugin to use for authentication.
name: An optional name for the plugin.
Returns:
A CallCredentials.
"""
```
The `CallCredentials` object can be passed directly into an RPC like:
```Python
call_credentials = grpc.metadata_call_credentials(my_foo_plugin)
stub.FooRpc(request, credentials=call_credentials)
```
Or you can use `ChannelCredentials` and `CallCredentials` at the same time by
combining them:
```Python
channel_credentials = ...
call_credentials = ...
composite_credentials = grpc.composite_channel_credentials(
channel_credential,
call_credentials)
channel = grpc.secure_channel(server_address, composite_credentials)
```
It is also possible to apply multiple `CallCredentials` to a single RPC:
```Python
call_credentials_foo = ...
call_credentials_bar = ...
call_credentials = grpc.composite_call_credentials(
call_credentials_foo,
call_credentials_bar)
stub.FooRpc(request, credentials=call_credentials)
```

@ -0,0 +1,31 @@
# Copyright 2019 The gRPC Authors
#
# 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.
"""Loading SSL credentials for gRPC Python authentication example."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os
def _load_credential_from_file(filepath):
real_path = os.path.join(os.path.dirname(__file__), filepath)
with open(real_path, 'rb') as f:
return f.read()
SERVER_CERTIFICATE = _load_credential_from_file('credentials/localhost.crt')
SERVER_CERTIFICATE_KEY = _load_credential_from_file('credentials/localhost.key')
ROOT_CERTIFICATE = _load_credential_from_file('credentials/root.crt')

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDFjCCAf4CCQCzrLIhrWa55zANBgkqhkiG9w0BAQsFADBCMQswCQYDVQQGEwJV
UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEPMA0GA1UECgwGR29vZ2xlMQ0wCwYDVQQL
DARnUlBDMCAXDTE5MDYyNDIyMjIzM1oYDzIxMTkwNTMxMjIyMjMzWjBWMQswCQYD
VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEPMA0GA1UECgwGR29vZ2xlMQ0w
CwYDVQQLDARnUlBDMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCtCW0TjugnIUu8BEVIYvdMP+/2GENQDjZhZ8eKR5C6
toDGbgjsDtt/GxISAg4cg70fIvy0XolnGPZodvfHDM4lJ7yHBOdZD8TXQoE6okR7
HZuLUJ20M0pXgWqtRewKRUjuYsSDXBnzLiZw1dcv9nGpo+Bqa8NonpiGRRpEkshF
D6T9KU9Ts/x+wMQBIra2Gj0UMh79jPhUuxcYAQA0JQGivnOtdwuPiumpnUT8j8h6
tWg5l01EsCZWJecCF85KnGpJEVYPyPqBqGsy0nGS9plGotOWF87+jyUQt+KD63xA
aBmTro86mKDDKEK4JvzjVeMGz2UbVcLPiiZnErTFaiXJAgMBAAEwDQYJKoZIhvcN
AQELBQADggEBAKsDgOPCWp5WCy17vJbRlgfgk05sVNIHZtzrmdswjBmvSg8MUpep
XqcPNUpsljAXsf9UM5IFEMRdilUsFGWvHjBEtNAW8WUK9UV18WRuU//0w1Mp5HAN
xUEKb4BoyZr65vlCnTR+AR5c9FfPvLibhr5qHs2RA8Y3GyLOcGqBWed87jhdQLCc
P1bxB+96le5JeXq0tw215lxonI2/3ZYVK4/ok9gwXrQoWm8YieJqitk/ZQ4S17/4
pynHtDfdxLn23EXeGx+UTxJGfpRmhEZdJ+MN7QGYoomzx5qS5XoYKxRNrDlirJpr
OqXIn8E1it+6d5gOZfuHawcNGhRLplE/pfA=
-----END CERTIFICATE-----

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEArQltE47oJyFLvARFSGL3TD/v9hhDUA42YWfHikeQuraAxm4I
7A7bfxsSEgIOHIO9HyL8tF6JZxj2aHb3xwzOJSe8hwTnWQ/E10KBOqJEex2bi1Cd
tDNKV4FqrUXsCkVI7mLEg1wZ8y4mcNXXL/ZxqaPgamvDaJ6YhkUaRJLIRQ+k/SlP
U7P8fsDEASK2tho9FDIe/Yz4VLsXGAEANCUBor5zrXcLj4rpqZ1E/I/IerVoOZdN
RLAmViXnAhfOSpxqSRFWD8j6gahrMtJxkvaZRqLTlhfO/o8lELfig+t8QGgZk66P
OpigwyhCuCb841XjBs9lG1XCz4omZxK0xWolyQIDAQABAoIBADeq/Kh6JT3RfGf0
h8WN8TlaqHxnueAbcmtL0+oss+cdp7gu1jf7X6o4r0uT1a5ew40s2Fe+wj2kzkE1
ZOlouTlC22gkr7j7Vbxa7PBMG/Pvxoa/XL0IczZLsGImSJXVTG1E4SvRiZeulTdf
1GbdxhtpWV1jZe5Wd4Na3+SHxF5S7m3PrHiZlYdz1ND+8XZs1NlL9+ej72qSFul9
t/QjMWJ9pky/Wad5abnRLRyOsg+BsgnXbkUy2rD89ZxFMLda9pzXo3TPyAlBHonr
mkEsE4eRMWMpjBM79JbeyDdHn/cs/LjAZrzeDf7ugXr2CHQpKaM5O0PsNHezJII9
L5kCfzECgYEA4M/rz1UP1/BJoSqigUlSs0tPAg8a5UlkVsh6Osuq72IPNo8qg/Fw
oV/IiIS+q+obRcFj1Od3PGdTpCJwW5dzd2fXBQGmGdj0HucnCrs13RtBh91JiF5i
y/YYI9KfgOG2ZT9gG68T0gTs6jRrS3Qd83npqjrkJqMOd7s00MK9tUcCgYEAxQq7
T541oCYHSBRIIb0IrR25krZy9caxzCqPDwOcuuhaCqCiaq+ATvOWlSfgecm4eH0K
PCH0xlWxG0auPEwm4pA8+/WR/XJwscPZMuoht1EoKy1his4eKx/s7hHNeO6KOF0V
Y/zqIiuZnEwUoKbn7EqqNFSTT65PJKyGsICJFG8CgYAfaw9yl1myfQNdQb8aQGwN
YJ33FLNWje427qeeZe5KrDKiFloDvI9YDjHRWnPnRL1w/zj7fSm9yFb5HlMDieP6
MQnsyjEzdY2QcA+VwVoiv3dmDHgFVeOKy6bOAtaFxYWfGr9MvygO9t9BT/gawGyb
JVORlc9i0vDnrMMR1dV7awKBgBpTWLtGc/u1mPt0Wj7HtsUKV6TWY32a0l5owTxM
S0BdksogtBJ06DukJ9Y9wawD23WdnyRxlPZ6tHLkeprrwbY7dypioOKvy4a0l+xJ
g7+uRCOgqIuXBkjUtx8HmeAyXp0xMo5tWArAsIFFWOwt4IadYygitJvMuh44PraO
NcJZAoGADEiV0dheXUCVr8DrtSom8DQMj92/G/FIYjXL8OUhh0+F+YlYP0+F8PEU
yYIWEqL/S5tVKYshimUXQa537JcRKsTVJBG/ZKD2kuqgOc72zQy3oplimXeJDCXY
h2eAQ0u8GN6tN9C4t8Kp4a3y6FGsxgu+UTxdnL3YQ+yHAVhtCzo=
-----END RSA PRIVATE KEY-----

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDWTCCAkGgAwIBAgIJAPOConZMwykwMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMQ8wDQYDVQQKDAZHb29nbGUxDTAL
BgNVBAsMBGdSUEMwIBcNMTkwNjI0MjIyMDA3WhgPMjExOTA1MzEyMjIwMDdaMEIx
CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMQ8wDQYDVQQKDAZHb29n
bGUxDTALBgNVBAsMBGdSUEMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQCwqei3TfyLidnQNDJ2lierMYo229K92DuORni7nSjJQ59Jc3dNMsmqGQJjCD8o
6mTlKM/oCbs27Wpx+OxcOLvT95j2kiDGca1fCvaMdguIod09SWiyMpv/hp0trLv7
NJIKHznath6rHYX2Ii3fZ1yCPzyQbEPSAA+GNpoNm1v1ZWmWKke9v7vLlS3inNlW
Mt9jepK7DrtbNZnVDjeItnppBSbVYRMxIyNHkepFbqXx5TpkCvl4M4XQZw9bfSxQ
i3WZ3q+T1Tw//OUdPNc+OfMhu0MA0QoMwikskP0NaIC3dbJZ5Ogx0RcnaB4E+9C6
O/znUEh3WuKVl5HXBF+UwWoFAgMBAAGjUDBOMB0GA1UdDgQWBBRm3JIgzgK4G97J
fbMGatWMZc7V3jAfBgNVHSMEGDAWgBRm3JIgzgK4G97JfbMGatWMZc7V3jAMBgNV
HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCNiV8x41if094ry2srS0YucpiN
3rTPk08FOLsENTMYai524TGXJti1P6ofGr5KXCL0uxTByHE3fEiMMud2TIY5iHQo
Y4mzDTTcb+Q7yKHwYZMlcp6nO8W+NeY5t+S0JPHhb8deKWepcN2UpXBUYQLw7AiE
l96T9Gi+vC9h/XE5IVwHFQXTxf5UYzXtW1nfapvrOONg/ms41dgmrRKIi+knWfiJ
FdHpHX2sfDAoJtnpEISX+nxRGNVTLY64utXWm4yxaZJshvy2s8zWJgRg7rtwAhTT
Np9E9MnihXLEmDI4Co9XlLPJyZFmqImsbmVuKFeQOCiLAoPJaMI2lbi7fiTo
-----END CERTIFICATE-----

@ -0,0 +1,105 @@
# Copyright 2019 The gRPC Authors
#
# 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.
"""Client of the Python example of customizing authentication mechanism."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import argparse
import contextlib
import logging
import grpc
from examples import helloworld_pb2
from examples import helloworld_pb2_grpc
from examples.python.auth import _credentials
_LOGGER = logging.getLogger(__name__)
_LOGGER.setLevel(logging.INFO)
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
_SERVER_ADDR_TEMPLATE = 'localhost:%d'
_SIGNATURE_HEADER_KEY = 'x-signature'
class AuthGateway(grpc.AuthMetadataPlugin):
def __call__(self, context, callback):
"""Implements authentication by passing metadata to a callback.
Implementations of this method must not block.
Args:
context: An AuthMetadataContext providing information on the RPC that
the plugin is being called to authenticate.
callback: An AuthMetadataPluginCallback to be invoked either
synchronously or asynchronously.
"""
# Example AuthMetadataContext object:
# AuthMetadataContext(
# service_url=u'https://localhost:50051/helloworld.Greeter',
# method_name=u'SayHello')
signature = context.method_name[::-1]
callback(((_SIGNATURE_HEADER_KEY, signature),), None)
@contextlib.contextmanager
def create_client_channel(addr):
# Call credential object will be invoked for every single RPC
call_credentials = grpc.metadata_call_credentials(
AuthGateway(), name='auth gateway')
# Channel credential will be valid for the entire channel
channel_credential = grpc.ssl_channel_credentials(
_credentials.ROOT_CERTIFICATE)
# Combining channel credentials and call credentials together
composite_credentials = grpc.composite_channel_credentials(
channel_credential,
call_credentials,
)
channel = grpc.secure_channel(addr, composite_credentials)
yield channel
def send_rpc(channel):
stub = helloworld_pb2_grpc.GreeterStub(channel)
request = helloworld_pb2.HelloRequest(name='you')
try:
response = stub.SayHello(request)
except grpc.RpcError as rpc_error:
_LOGGER.error('Received error: %s', rpc_error)
return rpc_error
else:
_LOGGER.info('Received message: %s', response)
return response
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'--port',
nargs='?',
type=int,
default=50051,
help='the address of server')
args = parser.parse_args()
with create_client_channel(_SERVER_ADDR_TEMPLATE % args.port) as channel:
send_rpc(channel)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
main()

@ -0,0 +1,110 @@
# Copyright 2019 The gRPC Authors
#
# 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.
"""Server of the Python example of customizing authentication mechanism."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import argparse
import contextlib
import logging
import time
from concurrent import futures
import grpc
from examples import helloworld_pb2
from examples import helloworld_pb2_grpc
from examples.python.auth import _credentials
_LOGGER = logging.getLogger(__name__)
_LOGGER.setLevel(logging.INFO)
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
_LISTEN_ADDRESS_TEMPLATE = 'localhost:%d'
_SIGNATURE_HEADER_KEY = 'x-signature'
class SignatureValidationInterceptor(grpc.ServerInterceptor):
def __init__(self):
def abort(ignored_request, context):
context.abort(grpc.StatusCode.UNAUTHENTICATED, 'Invalid signature')
self._abortion = grpc.unary_unary_rpc_method_handler(abort)
def intercept_service(self, continuation, handler_call_details):
# Example HandlerCallDetails object:
# _HandlerCallDetails(
# method=u'/helloworld.Greeter/SayHello',
# invocation_metadata=...)
method_name = handler_call_details.method.split('/')[-1]
expected_metadata = (_SIGNATURE_HEADER_KEY, method_name[::-1])
if expected_metadata in handler_call_details.invocation_metadata:
return continuation(handler_call_details)
else:
return self._abortion
class SimpleGreeter(helloworld_pb2_grpc.GreeterServicer):
def SayHello(self, request, unused_context):
return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name)
@contextlib.contextmanager
def run_server(port):
# Bind interceptor to server
server = grpc.server(
futures.ThreadPoolExecutor(),
interceptors=(SignatureValidationInterceptor(),))
helloworld_pb2_grpc.add_GreeterServicer_to_server(SimpleGreeter(), server)
# Loading credentials
server_credentials = grpc.ssl_server_credentials(((
_credentials.SERVER_CERTIFICATE_KEY,
_credentials.SERVER_CERTIFICATE,
),))
# Pass down credentials
port = server.add_secure_port(_LISTEN_ADDRESS_TEMPLATE % port,
server_credentials)
server.start()
try:
yield port
finally:
server.stop(0)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'--port', nargs='?', type=int, default=50051, help='the listening port')
args = parser.parse_args()
with run_server(args.port) as port:
logging.info('Server is listening at port :%d', port)
try:
while True:
time.sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
pass
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
main()

@ -0,0 +1,56 @@
# Copyright 2019 The gRPC Authors
#
# 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.
"""Test for gRPC Python authentication example."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import unittest
import grpc
from examples.python.auth import _credentials
from examples.python.auth import customized_auth_client
from examples.python.auth import customized_auth_server
_SERVER_ADDR_TEMPLATE = 'localhost:%d'
class AuthExampleTest(unittest.TestCase):
def test_successful_call(self):
with customized_auth_server.run_server(0) as port:
with customized_auth_client.create_client_channel(
_SERVER_ADDR_TEMPLATE % port) as channel:
customized_auth_client.send_rpc(channel)
# No unhandled exception raised, test passed!
def test_no_channel_credential(self):
with customized_auth_server.run_server(0) as port:
with grpc.insecure_channel(_SERVER_ADDR_TEMPLATE % port) as channel:
resp = customized_auth_client.send_rpc(channel)
self.assertEqual(resp.code(), grpc.StatusCode.UNAVAILABLE)
def test_no_call_credential(self):
with customized_auth_server.run_server(0) as port:
channel_credential = grpc.ssl_channel_credentials(
_credentials.ROOT_CERTIFICATE)
with grpc.secure_channel(_SERVER_ADDR_TEMPLATE % port,
channel_credential) as channel:
resp = customized_auth_client.send_rpc(channel)
self.assertEqual(resp.code(), grpc.StatusCode.UNAUTHENTICATED)
if __name__ == '__main__':
unittest.main(verbosity=2)
Loading…
Cancel
Save