Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(10)

Side by Side Diff: client/third_party/google/auth/jwt.py

Issue 2953253003: Replace custom blob gRPC API with ByteStream (Closed)
Patch Set: Import ndb directly to test code Created 3 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2016 Google Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """JSON Web Tokens
16
17 Provides support for creating (encoding) and verifying (decoding) JWTs,
18 especially JWTs generated and consumed by Google infrastructure.
19
20 See `rfc7519`_ for more details on JWTs.
21
22 To encode a JWT use :func:`encode`::
23
24 from google.auth import crypto
25 from google.auth import jwt
26
27 signer = crypt.Signer(private_key)
28 payload = {'some': 'payload'}
29 encoded = jwt.encode(signer, payload)
30
31 To decode a JWT and verify claims use :func:`decode`::
32
33 claims = jwt.decode(encoded, certs=public_certs)
34
35 You can also skip verification::
36
37 claims = jwt.decode(encoded, verify=False)
38
39 .. _rfc7519: https://tools.ietf.org/html/rfc7519
40
41 """
42
43 import base64
44 import collections
45 import copy
46 import datetime
47 import json
48
49 import cachetools
50 from six.moves import urllib
51
52 from google.auth import _helpers
53 from google.auth import _service_account_info
54 from google.auth import crypt
55 from google.auth import exceptions
56 import google.auth.credentials
57
58 _DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
59 _DEFAULT_MAX_CACHE_SIZE = 10
60
61
62 def encode(signer, payload, header=None, key_id=None):
63 """Make a signed JWT.
64
65 Args:
66 signer (google.auth.crypt.Signer): The signer used to sign the JWT.
67 payload (Mapping[str, str]): The JWT payload.
68 header (Mapping[str, str]): Additional JWT header payload.
69 key_id (str): The key id to add to the JWT header. If the
70 signer has a key id it will be used as the default. If this is
71 specified it will override the signer's key id.
72
73 Returns:
74 bytes: The encoded JWT.
75 """
76 if header is None:
77 header = {}
78
79 if key_id is None:
80 key_id = signer.key_id
81
82 header.update({'typ': 'JWT', 'alg': 'RS256'})
83
84 if key_id is not None:
85 header['kid'] = key_id
86
87 segments = [
88 base64.urlsafe_b64encode(json.dumps(header).encode('utf-8')),
89 base64.urlsafe_b64encode(json.dumps(payload).encode('utf-8')),
90 ]
91
92 signing_input = b'.'.join(segments)
93 signature = signer.sign(signing_input)
94 segments.append(base64.urlsafe_b64encode(signature))
95
96 return b'.'.join(segments)
97
98
99 def _decode_jwt_segment(encoded_section):
100 """Decodes a single JWT segment."""
101 section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section)
102 try:
103 return json.loads(section_bytes.decode('utf-8'))
104 except ValueError:
105 raise ValueError('Can\'t parse segment: {0}'.format(section_bytes))
106
107
108 def _unverified_decode(token):
109 """Decodes a token and does no verification.
110
111 Args:
112 token (Union[str, bytes]): The encoded JWT.
113
114 Returns:
115 Tuple[str, str, str, str]: header, payload, signed_section, and
116 signature.
117
118 Raises:
119 ValueError: if there are an incorrect amount of segments in the token.
120 """
121 token = _helpers.to_bytes(token)
122
123 if token.count(b'.') != 2:
124 raise ValueError(
125 'Wrong number of segments in token: {0}'.format(token))
126
127 encoded_header, encoded_payload, signature = token.split(b'.')
128 signed_section = encoded_header + b'.' + encoded_payload
129 signature = _helpers.padded_urlsafe_b64decode(signature)
130
131 # Parse segments
132 header = _decode_jwt_segment(encoded_header)
133 payload = _decode_jwt_segment(encoded_payload)
134
135 return header, payload, signed_section, signature
136
137
138 def decode_header(token):
139 """Return the decoded header of a token.
140
141 No verification is done. This is useful to extract the key id from
142 the header in order to acquire the appropriate certificate to verify
143 the token.
144
145 Args:
146 token (Union[str, bytes]): the encoded JWT.
147
148 Returns:
149 Mapping: The decoded JWT header.
150 """
151 header, _, _, _ = _unverified_decode(token)
152 return header
153
154
155 def _verify_iat_and_exp(payload):
156 """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
157 payload.
158
159 Args:
160 payload (Mapping[str, str]): The JWT payload.
161
162 Raises:
163 ValueError: if any checks failed.
164 """
165 now = _helpers.datetime_to_secs(_helpers.utcnow())
166
167 # Make sure the iat and exp claims are present.
168 for key in ('iat', 'exp'):
169 if key not in payload:
170 raise ValueError(
171 'Token does not contain required claim {}'.format(key))
172
173 # Make sure the token wasn't issued in the future.
174 iat = payload['iat']
175 # Err on the side of accepting a token that is slightly early to account
176 # for clock skew.
177 earliest = iat - _helpers.CLOCK_SKEW_SECS
178 if now < earliest:
179 raise ValueError('Token used too early, {} < {}'.format(now, iat))
180
181 # Make sure the token wasn't issued in the past.
182 exp = payload['exp']
183 # Err on the side of accepting a token that is slightly out of date
184 # to account for clow skew.
185 latest = exp + _helpers.CLOCK_SKEW_SECS
186 if latest < now:
187 raise ValueError('Token expired, {} < {}'.format(latest, now))
188
189
190 def decode(token, certs=None, verify=True, audience=None):
191 """Decode and verify a JWT.
192
193 Args:
194 token (str): The encoded JWT.
195 certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
196 certificate used to validate the JWT signatyre. If bytes or string,
197 it must the the public key certificate in PEM format. If a mapping,
198 it must be a mapping of key IDs to public key certificates in PEM
199 format. The mapping must contain the same key ID that's specified
200 in the token's header.
201 verify (bool): Whether to perform signature and claim validation.
202 Verification is done by default.
203 audience (str): The audience claim, 'aud', that this JWT should
204 contain. If None then the JWT's 'aud' parameter is not verified.
205
206 Returns:
207 Mapping[str, str]: The deserialized JSON payload in the JWT.
208
209 Raises:
210 ValueError: if any verification checks failed.
211 """
212 header, payload, signed_section, signature = _unverified_decode(token)
213
214 if not verify:
215 return payload
216
217 # If certs is specified as a dictionary of key IDs to certificates, then
218 # use the certificate identified by the key ID in the token header.
219 if isinstance(certs, collections.Mapping):
220 key_id = header.get('kid')
221 if key_id:
222 if key_id not in certs:
223 raise ValueError(
224 'Certificate for key id {} not found.'.format(key_id))
225 certs_to_check = [certs[key_id]]
226 # If there's no key id in the header, check against all of the certs.
227 else:
228 certs_to_check = certs.values()
229 else:
230 certs_to_check = certs
231
232 # Verify that the signature matches the message.
233 if not crypt.verify_signature(signed_section, signature, certs_to_check):
234 raise ValueError('Could not verify token signature.')
235
236 # Verify the issued at and created times in the payload.
237 _verify_iat_and_exp(payload)
238
239 # Check audience.
240 if audience is not None:
241 claim_audience = payload.get('aud')
242 if audience != claim_audience:
243 raise ValueError(
244 'Token has wrong audience {}, expected {}'.format(
245 claim_audience, audience))
246
247 return payload
248
249
250 class Credentials(google.auth.credentials.Signing,
251 google.auth.credentials.Credentials):
252 """Credentials that use a JWT as the bearer token.
253
254 These credentials require an "audience" claim. This claim identifies the
255 intended recipient of the bearer token.
256
257 The constructor arguments determine the claims for the JWT that is
258 sent with requests. Usually, you'll construct these credentials with
259 one of the helper constructors as shown in the next section.
260
261 To create JWT credentials using a Google service account private key
262 JSON file::
263
264 audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
265 credentials = jwt.Credentials.from_service_account_file(
266 'service-account.json',
267 audience=audience)
268
269 If you already have the service account file loaded and parsed::
270
271 service_account_info = json.load(open('service_account.json'))
272 credentials = jwt.Credentials.from_service_account_info(
273 service_account_info,
274 audience=audience)
275
276 Both helper methods pass on arguments to the constructor, so you can
277 specify the JWT claims::
278
279 credentials = jwt.Credentials.from_service_account_file(
280 'service-account.json',
281 audience=audience,
282 additional_claims={'meta': 'data'})
283
284 You can also construct the credentials directly if you have a
285 :class:`~google.auth.crypt.Signer` instance::
286
287 credentials = jwt.Credentials(
288 signer,
289 issuer='your-issuer',
290 subject='your-subject',
291 audience=audience)
292
293 The claims are considered immutable. If you want to modify the claims,
294 you can easily create another instance using :meth:`with_claims`::
295
296 new_audience = (
297 'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
298 new_credentials = credentials.with_claims(audience=new_audience)
299 """
300
301 def __init__(self, signer, issuer, subject, audience,
302 additional_claims=None,
303 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS):
304 """
305 Args:
306 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
307 issuer (str): The `iss` claim.
308 subject (str): The `sub` claim.
309 audience (str): the `aud` claim. The intended audience for the
310 credentials.
311 additional_claims (Mapping[str, str]): Any additional claims for
312 the JWT payload.
313 token_lifetime (int): The amount of time in seconds for
314 which the token is valid. Defaults to 1 hour.
315 """
316 super(Credentials, self).__init__()
317 self._signer = signer
318 self._issuer = issuer
319 self._subject = subject
320 self._audience = audience
321 self._token_lifetime = token_lifetime
322
323 if additional_claims is None:
324 additional_claims = {}
325
326 self._additional_claims = additional_claims
327
328 @classmethod
329 def _from_signer_and_info(cls, signer, info, **kwargs):
330 """Creates a Credentials instance from a signer and service account
331 info.
332
333 Args:
334 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
335 info (Mapping[str, str]): The service account info.
336 kwargs: Additional arguments to pass to the constructor.
337
338 Returns:
339 google.auth.jwt.Credentials: The constructed credentials.
340
341 Raises:
342 ValueError: If the info is not in the expected format.
343 """
344 kwargs.setdefault('subject', info['client_email'])
345 kwargs.setdefault('issuer', info['client_email'])
346 return cls(signer, **kwargs)
347
348 @classmethod
349 def from_service_account_info(cls, info, **kwargs):
350 """Creates an Credentials instance from a dictionary.
351
352 Args:
353 info (Mapping[str, str]): The service account info in Google
354 format.
355 kwargs: Additional arguments to pass to the constructor.
356
357 Returns:
358 google.auth.jwt.Credentials: The constructed credentials.
359
360 Raises:
361 ValueError: If the info is not in the expected format.
362 """
363 signer = _service_account_info.from_dict(
364 info, require=['client_email'])
365 return cls._from_signer_and_info(signer, info, **kwargs)
366
367 @classmethod
368 def from_service_account_file(cls, filename, **kwargs):
369 """Creates a Credentials instance from a service account .json file
370 in Google format.
371
372 Args:
373 filename (str): The path to the service account .json file.
374 kwargs: Additional arguments to pass to the constructor.
375
376 Returns:
377 google.auth.jwt.Credentials: The constructed credentials.
378 """
379 info, signer = _service_account_info.from_filename(
380 filename, require=['client_email'])
381 return cls._from_signer_and_info(signer, info, **kwargs)
382
383 @classmethod
384 def from_signing_credentials(cls, credentials, audience, **kwargs):
385 """Creates a new :class:`google.auth.jwt.Credentials` instance from an
386 existing :class:`google.auth.credentials.Signing` instance.
387
388 The new instance will use the same signer as the existing instance and
389 will use the existing instance's signer email as the issuer and
390 subject by default.
391
392 Example::
393
394 svc_creds = service_account.Credentials.from_service_account_file(
395 'service_account.json')
396 audience = (
397 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher')
398 jwt_creds = jwt.Credentials.from_signing_credentials(
399 svc_creds, audience=audience)
400
401 Args:
402 credentials (google.auth.credentials.Signing): The credentials to
403 use to construct the new credentials.
404 audience (str): the `aud` claim. The intended audience for the
405 credentials.
406 kwargs: Additional arguments to pass to the constructor.
407
408 Returns:
409 google.auth.jwt.Credentials: A new Credentials instance.
410 """
411 kwargs.setdefault('issuer', credentials.signer_email)
412 kwargs.setdefault('subject', credentials.signer_email)
413 return cls(
414 credentials.signer,
415 audience=audience,
416 **kwargs)
417
418 def with_claims(self, issuer=None, subject=None, audience=None,
419 additional_claims=None):
420 """Returns a copy of these credentials with modified claims.
421
422 Args:
423 issuer (str): The `iss` claim. If unspecified the current issuer
424 claim will be used.
425 subject (str): The `sub` claim. If unspecified the current subject
426 claim will be used.
427 audience (str): the `aud` claim. If unspecified the current
428 audience claim will be used.
429 additional_claims (Mapping[str, str]): Any additional claims for
430 the JWT payload. This will be merged with the current
431 additional claims.
432
433 Returns:
434 google.auth.jwt.Credentials: A new credentials instance.
435 """
436 new_additional_claims = copy.deepcopy(self._additional_claims)
437 new_additional_claims.update(additional_claims or {})
438
439 return Credentials(
440 self._signer,
441 issuer=issuer if issuer is not None else self._issuer,
442 subject=subject if subject is not None else self._subject,
443 audience=audience if audience is not None else self._audience,
444 additional_claims=new_additional_claims)
445
446 def _make_jwt(self):
447 """Make a signed JWT.
448
449 Returns:
450 Tuple[bytes, datetime]: The encoded JWT and the expiration.
451 """
452 now = _helpers.utcnow()
453 lifetime = datetime.timedelta(seconds=self._token_lifetime)
454 expiry = now + lifetime
455
456 payload = {
457 'iss': self._issuer,
458 'sub': self._subject,
459 'iat': _helpers.datetime_to_secs(now),
460 'exp': _helpers.datetime_to_secs(expiry),
461 'aud': self._audience,
462 }
463
464 payload.update(self._additional_claims)
465
466 jwt = encode(self._signer, payload)
467
468 return jwt, expiry
469
470 def refresh(self, request):
471 """Refreshes the access token.
472
473 Args:
474 request (Any): Unused.
475 """
476 # pylint: disable=unused-argument
477 # (pylint doesn't correctly recognize overridden methods.)
478 self.token, self.expiry = self._make_jwt()
479
480 @_helpers.copy_docstring(google.auth.credentials.Signing)
481 def sign_bytes(self, message):
482 return self._signer.sign(message)
483
484 @property
485 @_helpers.copy_docstring(google.auth.credentials.Signing)
486 def signer_email(self):
487 return self._issuer
488
489 @property
490 @_helpers.copy_docstring(google.auth.credentials.Signing)
491 def signer(self):
492 return self._signer
493
494
495 class OnDemandCredentials(
496 google.auth.credentials.Signing,
497 google.auth.credentials.Credentials):
498 """On-demand JWT credentials.
499
500 Like :class:`Credentials`, this class uses a JWT as the bearer token for
501 authentication. However, this class does not require the audience at
502 construction time. Instead, it will generate a new token on-demand for
503 each request using the request URI as the audience. It caches tokens
504 so that multiple requests to the same URI do not incur the overhead
505 of generating a new token every time.
506
507 This behavior is especially useful for `gRPC`_ clients. A gRPC service may
508 have multiple audience and gRPC clients may not know all of the audiences
509 required for accessing a particular service. With these credentials,
510 no knowledge of the audiences is required ahead of time.
511
512 .. _grpc: http://www.grpc.io/
513 """
514
515 def __init__(self, signer, issuer, subject,
516 additional_claims=None,
517 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
518 max_cache_size=_DEFAULT_MAX_CACHE_SIZE):
519 """
520 Args:
521 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
522 issuer (str): The `iss` claim.
523 subject (str): The `sub` claim.
524 additional_claims (Mapping[str, str]): Any additional claims for
525 the JWT payload.
526 token_lifetime (int): The amount of time in seconds for
527 which the token is valid. Defaults to 1 hour.
528 max_cache_size (int): The maximum number of JWT tokens to keep in
529 cache. Tokens are cached using :class:`cachetools.LRUCache`.
530 """
531 super(OnDemandCredentials, self).__init__()
532 self._signer = signer
533 self._issuer = issuer
534 self._subject = subject
535 self._token_lifetime = token_lifetime
536
537 if additional_claims is None:
538 additional_claims = {}
539
540 self._additional_claims = additional_claims
541 self._cache = cachetools.LRUCache(maxsize=max_cache_size)
542
543 @classmethod
544 def _from_signer_and_info(cls, signer, info, **kwargs):
545 """Creates an OnDemandCredentials instance from a signer and service
546 account info.
547
548 Args:
549 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
550 info (Mapping[str, str]): The service account info.
551 kwargs: Additional arguments to pass to the constructor.
552
553 Returns:
554 google.auth.jwt.OnDemandCredentials: The constructed credentials.
555
556 Raises:
557 ValueError: If the info is not in the expected format.
558 """
559 kwargs.setdefault('subject', info['client_email'])
560 kwargs.setdefault('issuer', info['client_email'])
561 return cls(signer, **kwargs)
562
563 @classmethod
564 def from_service_account_info(cls, info, **kwargs):
565 """Creates an OnDemandCredentials instance from a dictionary.
566
567 Args:
568 info (Mapping[str, str]): The service account info in Google
569 format.
570 kwargs: Additional arguments to pass to the constructor.
571
572 Returns:
573 google.auth.jwt.OnDemandCredentials: The constructed credentials.
574
575 Raises:
576 ValueError: If the info is not in the expected format.
577 """
578 signer = _service_account_info.from_dict(
579 info, require=['client_email'])
580 return cls._from_signer_and_info(signer, info, **kwargs)
581
582 @classmethod
583 def from_service_account_file(cls, filename, **kwargs):
584 """Creates an OnDemandCredentials instance from a service account .json
585 file in Google format.
586
587 Args:
588 filename (str): The path to the service account .json file.
589 kwargs: Additional arguments to pass to the constructor.
590
591 Returns:
592 google.auth.jwt.OnDemandCredentials: The constructed credentials.
593 """
594 info, signer = _service_account_info.from_filename(
595 filename, require=['client_email'])
596 return cls._from_signer_and_info(signer, info, **kwargs)
597
598 @classmethod
599 def from_signing_credentials(cls, credentials, **kwargs):
600 """Creates a new :class:`google.auth.jwt.OnDemandCredentials` instance
601 from an existing :class:`google.auth.credentials.Signing` instance.
602
603 The new instance will use the same signer as the existing instance and
604 will use the existing instance's signer email as the issuer and
605 subject by default.
606
607 Example::
608
609 svc_creds = service_account.Credentials.from_service_account_file(
610 'service_account.json')
611 jwt_creds = jwt.OnDemandCredentials.from_signing_credentials(
612 svc_creds)
613
614 Args:
615 credentials (google.auth.credentials.Signing): The credentials to
616 use to construct the new credentials.
617 kwargs: Additional arguments to pass to the constructor.
618
619 Returns:
620 google.auth.jwt.Credentials: A new Credentials instance.
621 """
622 kwargs.setdefault('issuer', credentials.signer_email)
623 kwargs.setdefault('subject', credentials.signer_email)
624 return cls(credentials.signer, **kwargs)
625
626 def with_claims(self, issuer=None, subject=None, additional_claims=None):
627 """Returns a copy of these credentials with modified claims.
628
629 Args:
630 issuer (str): The `iss` claim. If unspecified the current issuer
631 claim will be used.
632 subject (str): The `sub` claim. If unspecified the current subject
633 claim will be used.
634 additional_claims (Mapping[str, str]): Any additional claims for
635 the JWT payload. This will be merged with the current
636 additional claims.
637
638 Returns:
639 google.auth.jwt.OnDemandCredentials: A new credentials instance.
640 """
641 new_additional_claims = copy.deepcopy(self._additional_claims)
642 new_additional_claims.update(additional_claims or {})
643
644 return OnDemandCredentials(
645 self._signer,
646 issuer=issuer if issuer is not None else self._issuer,
647 subject=subject if subject is not None else self._subject,
648 additional_claims=new_additional_claims,
649 max_cache_size=self._cache.maxsize)
650
651 @property
652 def valid(self):
653 """Checks the validity of the credentials.
654
655 These credentials are always valid because it generates tokens on
656 demand.
657 """
658 return True
659
660 def _make_jwt_for_audience(self, audience):
661 """Make a new JWT for the given audience.
662
663 Args:
664 audience (str): The intended audience.
665
666 Returns:
667 Tuple[bytes, datetime]: The encoded JWT and the expiration.
668 """
669 now = _helpers.utcnow()
670 lifetime = datetime.timedelta(seconds=self._token_lifetime)
671 expiry = now + lifetime
672
673 payload = {
674 'iss': self._issuer,
675 'sub': self._subject,
676 'iat': _helpers.datetime_to_secs(now),
677 'exp': _helpers.datetime_to_secs(expiry),
678 'aud': audience,
679 }
680
681 payload.update(self._additional_claims)
682
683 jwt = encode(self._signer, payload)
684
685 return jwt, expiry
686
687 def _get_jwt_for_audience(self, audience):
688 """Get a JWT For a given audience.
689
690 If there is already an existing, non-expired token in the cache for
691 the audience, that token is used. Otherwise, a new token will be
692 created.
693
694 Args:
695 audience (str): The intended audience.
696
697 Returns:
698 bytes: The encoded JWT.
699 """
700 token, expiry = self._cache.get(audience, (None, None))
701
702 if token is None or expiry < _helpers.utcnow():
703 token, expiry = self._make_jwt_for_audience(audience)
704 self._cache[audience] = token, expiry
705
706 return token
707
708 def refresh(self, request):
709 """Raises an exception, these credentials can not be directly
710 refreshed.
711
712 Args:
713 request (Any): Unused.
714
715 Raises:
716 google.auth.RefreshError
717 """
718 # pylint: disable=unused-argument
719 # (pylint doesn't correctly recognize overridden methods.)
720 raise exceptions.RefreshError(
721 'OnDemandCredentials can not be directly refreshed.')
722
723 def before_request(self, request, method, url, headers):
724 """Performs credential-specific before request logic.
725
726 Args:
727 request (Any): Unused. JWT credentials do not need to make an
728 HTTP request to refresh.
729 method (str): The request's HTTP method.
730 url (str): The request's URI. This is used as the audience claim
731 when generating the JWT.
732 headers (Mapping): The request's headers.
733 """
734 # pylint: disable=unused-argument
735 # (pylint doesn't correctly recognize overridden methods.)
736 parts = urllib.parse.urlsplit(url)
737 # Strip query string and fragment
738 audience = urllib.parse.urlunsplit(
739 (parts.scheme, parts.netloc, parts.path, None, None))
740 token = self._get_jwt_for_audience(audience)
741 self.apply(headers, token=token)
742
743 @_helpers.copy_docstring(google.auth.credentials.Signing)
744 def sign_bytes(self, message):
745 return self._signer.sign(message)
746
747 @property
748 @_helpers.copy_docstring(google.auth.credentials.Signing)
749 def signer_email(self):
750 return self._issuer
751
752 @property
753 @_helpers.copy_docstring(google.auth.credentials.Signing)
754 def signer(self):
755 return self._signer
OLDNEW
« no previous file with comments | « client/third_party/google/auth/iam.py ('k') | client/third_party/google/auth/transport/__init__.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698