3535 )
3636import requests .adapters # pylint: disable=ungrouped-imports
3737import requests .exceptions # pylint: disable=ungrouped-imports
38+ from requests .packages .urllib3 .util .ssl_ import (
39+ create_urllib3_context ,
40+ ) # pylint: disable=ungrouped-imports
3841import six # pylint: disable=ungrouped-imports
3942
4043from google .auth import exceptions
4144from google .auth import transport
45+ import google .auth .transport ._mtls_helper
4246
4347_LOGGER = logging .getLogger (__name__ )
4448
@@ -182,6 +186,52 @@ def __call__(
182186 six .raise_from (new_exc , caught_exc )
183187
184188
189+ class _MutualTlsAdapter (requests .adapters .HTTPAdapter ):
190+ """
191+ A TransportAdapter that enables mutual TLS.
192+
193+ Args:
194+ cert (bytes): client certificate in PEM format
195+ key (bytes): client private key in PEM format
196+
197+ Raises:
198+ ImportError: if certifi or pyOpenSSL is not installed
199+ OpenSSL.crypto.Error: if client cert or key is invalid
200+ """
201+
202+ def __init__ (self , cert , key ):
203+ import certifi
204+ from OpenSSL import crypto
205+ import urllib3 .contrib .pyopenssl
206+
207+ urllib3 .contrib .pyopenssl .inject_into_urllib3 ()
208+
209+ pkey = crypto .load_privatekey (crypto .FILETYPE_PEM , key )
210+ x509 = crypto .load_certificate (crypto .FILETYPE_PEM , cert )
211+
212+ ctx_poolmanager = create_urllib3_context ()
213+ ctx_poolmanager .load_verify_locations (cafile = certifi .where ())
214+ ctx_poolmanager ._ctx .use_certificate (x509 )
215+ ctx_poolmanager ._ctx .use_privatekey (pkey )
216+ self ._ctx_poolmanager = ctx_poolmanager
217+
218+ ctx_proxymanager = create_urllib3_context ()
219+ ctx_proxymanager .load_verify_locations (cafile = certifi .where ())
220+ ctx_proxymanager ._ctx .use_certificate (x509 )
221+ ctx_proxymanager ._ctx .use_privatekey (pkey )
222+ self ._ctx_proxymanager = ctx_proxymanager
223+
224+ super (_MutualTlsAdapter , self ).__init__ ()
225+
226+ def init_poolmanager (self , * args , ** kwargs ):
227+ kwargs ["ssl_context" ] = self ._ctx_poolmanager
228+ super (_MutualTlsAdapter , self ).init_poolmanager (* args , ** kwargs )
229+
230+ def proxy_manager_for (self , * args , ** kwargs ):
231+ kwargs ["ssl_context" ] = self ._ctx_proxymanager
232+ return super (_MutualTlsAdapter , self ).proxy_manager_for (* args , ** kwargs )
233+
234+
185235class AuthorizedSession (requests .Session ):
186236 """A Requests Session class with credentials.
187237
@@ -198,6 +248,49 @@ class AuthorizedSession(requests.Session):
198248 The underlying :meth:`request` implementation handles adding the
199249 credentials' headers to the request and refreshing credentials as needed.
200250
251+ This class also supports mutual TLS via :meth:`configure_mtls_channel`
252+ method. This method first tries to load client certificate and private key
253+ using the given client_cert_callabck; if callback is None or fails, it tries
254+ to load application default SSL credentials. Exceptions are raised if there
255+ are problems with the certificate, private key, or the loading process, so
256+ it should be called within a try/except block.
257+
258+ First we create an :class:`AuthorizedSession` instance and specify the endpoints::
259+
260+ regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
261+ mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
262+
263+ authed_session = AuthorizedSession(credentials)
264+
265+ Now we can pass a callback to :meth:`configure_mtls_channel`::
266+
267+ def my_cert_callback():
268+ # some code to load client cert bytes and private key bytes, both in
269+ # PEM format.
270+ some_code_to_load_client_cert_and_key()
271+ if loaded:
272+ return True, cert, key
273+ else:
274+ return False, None, None
275+
276+ # Always call configure_mtls_channel within a try/except block.
277+ try:
278+ authed_session.configure_mtls_channel(my_cert_callback)
279+ except:
280+ # handle exceptions.
281+
282+ if authed_session.is_mtls:
283+ response = authed_session.request('GET', mtls_endpoint)
284+ else:
285+ response = authed_session.request('GET', regular_endpoint)
286+
287+ You can alternatively use application default SSL credentials like this::
288+
289+ try:
290+ authed_session.configure_mtls_channel()
291+ except:
292+ # handle exceptions.
293+
201294 Args:
202295 credentials (google.auth.credentials.Credentials): The credentials to
203296 add to the request.
@@ -229,6 +322,7 @@ def __init__(
229322 self ._refresh_status_codes = refresh_status_codes
230323 self ._max_refresh_attempts = max_refresh_attempts
231324 self ._refresh_timeout = refresh_timeout
325+ self ._is_mtls = False
232326
233327 if auth_request is None :
234328 auth_request_session = requests .Session ()
@@ -247,6 +341,40 @@ def __init__(
247341 # credentials.refresh).
248342 self ._auth_request = auth_request
249343
344+ def configure_mtls_channel (self , client_cert_callback = None ):
345+ """Configure the client certificate and key for SSL connection.
346+
347+ If client certificate and key are successfully obtained (from the given
348+ client_cert_callabck or from application default SSL credentials), a
349+ :class:`_MutualTlsAdapter` instance will be mounted to "https://" prefix.
350+
351+ Args:
352+ client_cert_callabck (Optional[Callable[[], (bool, bytes, bytes)]]):
353+ The optional callback returns a boolean indicating if the call
354+ is successful, and the client certificate and private key bytes
355+ both in PEM format.
356+ If the call is not succesful, application default SSL credentials
357+ will be used.
358+
359+ Raises:
360+ ImportError: If certifi or pyOpenSSL is not installed.
361+ OpenSSL.crypto.Error: If client cert or key is invalid.
362+ OSError: If the cert provider command launch fails during the
363+ application default SSL credentials loading process.
364+ RuntimeError: If the cert provider command has a runtime error during
365+ the application default SSL credentials loading process.
366+ ValueError: If the context aware metadata file is malformed or the
367+ cert provider command doesn't produce both client certicate and
368+ key during the application default SSL credentials loading process.
369+ """
370+ self ._is_mtls , cert , key = google .auth .transport ._mtls_helper .get_client_cert_and_key (
371+ client_cert_callback
372+ )
373+
374+ if self ._is_mtls :
375+ mtls_adapter = _MutualTlsAdapter (cert , key )
376+ self .mount ("https://" , mtls_adapter )
377+
250378 def request (
251379 self ,
252380 method ,
@@ -361,3 +489,8 @@ def request(
361489 )
362490
363491 return response
492+
493+ @property
494+ def is_mtls (self ):
495+ """Indicates if the created SSL channel is mutual TLS."""
496+ return self ._is_mtls
0 commit comments