blob: bea70adf39e5afb1caa4e588f0827b4e76c4d7c3 [file] [log] [blame]
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -07001# 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
17Provides support for creating (encoding) and verifying (decoding) JWTs,
18especially JWTs generated and consumed by Google infrastructure.
19
20See `rfc7519`_ for more details on JWTs.
21
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -070022To encode a JWT use :func:`encode`::
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070023
Marco Rougeth7e1270b2018-02-28 20:13:56 -030024 from google.auth import crypt
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070025 from google.auth import jwt
26
27 signer = crypt.Signer(private_key)
28 payload = {'some': 'payload'}
29 encoded = jwt.encode(signer, payload)
30
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -070031To decode a JWT and verify claims use :func:`decode`::
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070032
33 claims = jwt.decode(encoded, certs=public_certs)
34
35You can also skip verification::
36
37 claims = jwt.decode(encoded, verify=False)
38
39.. _rfc7519: https://tools.ietf.org/html/rfc7519
40
41"""
42
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070043import collections
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -070044import copy
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -070045import datetime
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070046import json
47
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070048import cachetools
Danny Hermes895e3692017-11-09 11:35:57 -080049import six
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070050from six.moves import urllib
51
Jon Wayne Parrott54a85172016-10-17 11:27:37 -070052from google.auth import _helpers
Jon Wayne Parrott807032c2016-10-18 09:38:26 -070053from google.auth import _service_account_info
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070054from google.auth import crypt
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070055from google.auth import exceptions
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -080056import google.auth.credentials
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070057
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -070058_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070059_DEFAULT_MAX_CACHE_SIZE = 10
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070060
61
62def 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.
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -070067 payload (Mapping[str, str]): The JWT payload.
68 header (Mapping[str, str]): Additional JWT header payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070069 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 = [
Aditya Natrajae7e4f32019-02-15 00:02:10 -050088 _helpers.unpadded_urlsafe_b64encode(
89 json.dumps(header).encode('utf-8')
90 ),
91 _helpers.unpadded_urlsafe_b64encode(
92 json.dumps(payload).encode('utf-8')
93 ),
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070094 ]
95
96 signing_input = b'.'.join(segments)
97 signature = signer.sign(signing_input)
Aditya Natrajae7e4f32019-02-15 00:02:10 -050098 segments.append(
99 _helpers.unpadded_urlsafe_b64encode(signature)
100 )
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700101
102 return b'.'.join(segments)
103
104
105def _decode_jwt_segment(encoded_section):
106 """Decodes a single JWT segment."""
Jon Wayne Parrott97eb8702016-11-17 09:43:16 -0800107 section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700108 try:
109 return json.loads(section_bytes.decode('utf-8'))
Danny Hermes895e3692017-11-09 11:35:57 -0800110 except ValueError as caught_exc:
111 new_exc = ValueError('Can\'t parse segment: {0}'.format(section_bytes))
112 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700113
114
115def _unverified_decode(token):
116 """Decodes a token and does no verification.
117
118 Args:
119 token (Union[str, bytes]): The encoded JWT.
120
121 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800122 Tuple[str, str, str, str]: header, payload, signed_section, and
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700123 signature.
124
125 Raises:
126 ValueError: if there are an incorrect amount of segments in the token.
127 """
128 token = _helpers.to_bytes(token)
129
130 if token.count(b'.') != 2:
131 raise ValueError(
132 'Wrong number of segments in token: {0}'.format(token))
133
134 encoded_header, encoded_payload, signature = token.split(b'.')
135 signed_section = encoded_header + b'.' + encoded_payload
Jon Wayne Parrott97eb8702016-11-17 09:43:16 -0800136 signature = _helpers.padded_urlsafe_b64decode(signature)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700137
138 # Parse segments
139 header = _decode_jwt_segment(encoded_header)
140 payload = _decode_jwt_segment(encoded_payload)
141
142 return header, payload, signed_section, signature
143
144
145def decode_header(token):
146 """Return the decoded header of a token.
147
148 No verification is done. This is useful to extract the key id from
149 the header in order to acquire the appropriate certificate to verify
150 the token.
151
152 Args:
153 token (Union[str, bytes]): the encoded JWT.
154
155 Returns:
156 Mapping: The decoded JWT header.
157 """
158 header, _, _, _ = _unverified_decode(token)
159 return header
160
161
162def _verify_iat_and_exp(payload):
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700163 """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700164 payload.
165
166 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700167 payload (Mapping[str, str]): The JWT payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700168
169 Raises:
170 ValueError: if any checks failed.
171 """
172 now = _helpers.datetime_to_secs(_helpers.utcnow())
173
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700174 # Make sure the iat and exp claims are present.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700175 for key in ('iat', 'exp'):
176 if key not in payload:
177 raise ValueError(
178 'Token does not contain required claim {}'.format(key))
179
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700180 # Make sure the token wasn't issued in the future.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700181 iat = payload['iat']
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700182 # Err on the side of accepting a token that is slightly early to account
183 # for clock skew.
184 earliest = iat - _helpers.CLOCK_SKEW_SECS
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700185 if now < earliest:
186 raise ValueError('Token used too early, {} < {}'.format(now, iat))
187
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700188 # Make sure the token wasn't issued in the past.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700189 exp = payload['exp']
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700190 # Err on the side of accepting a token that is slightly out of date
191 # to account for clow skew.
192 latest = exp + _helpers.CLOCK_SKEW_SECS
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700193 if latest < now:
194 raise ValueError('Token expired, {} < {}'.format(latest, now))
195
196
197def decode(token, certs=None, verify=True, audience=None):
198 """Decode and verify a JWT.
199
200 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700201 token (str): The encoded JWT.
202 certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
203 certificate used to validate the JWT signatyre. If bytes or string,
204 it must the the public key certificate in PEM format. If a mapping,
205 it must be a mapping of key IDs to public key certificates in PEM
206 format. The mapping must contain the same key ID that's specified
207 in the token's header.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700208 verify (bool): Whether to perform signature and claim validation.
209 Verification is done by default.
210 audience (str): The audience claim, 'aud', that this JWT should
211 contain. If None then the JWT's 'aud' parameter is not verified.
212
213 Returns:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700214 Mapping[str, str]: The deserialized JSON payload in the JWT.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700215
216 Raises:
217 ValueError: if any verification checks failed.
218 """
219 header, payload, signed_section, signature = _unverified_decode(token)
220
221 if not verify:
222 return payload
223
224 # If certs is specified as a dictionary of key IDs to certificates, then
225 # use the certificate identified by the key ID in the token header.
226 if isinstance(certs, collections.Mapping):
227 key_id = header.get('kid')
228 if key_id:
229 if key_id not in certs:
230 raise ValueError(
231 'Certificate for key id {} not found.'.format(key_id))
232 certs_to_check = [certs[key_id]]
233 # If there's no key id in the header, check against all of the certs.
234 else:
235 certs_to_check = certs.values()
236 else:
237 certs_to_check = certs
238
239 # Verify that the signature matches the message.
240 if not crypt.verify_signature(signed_section, signature, certs_to_check):
241 raise ValueError('Could not verify token signature.')
242
243 # Verify the issued at and created times in the payload.
244 _verify_iat_and_exp(payload)
245
246 # Check audience.
247 if audience is not None:
248 claim_audience = payload.get('aud')
249 if audience != claim_audience:
250 raise ValueError(
251 'Token has wrong audience {}, expected {}'.format(
252 claim_audience, audience))
253
254 return payload
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700255
256
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800257class Credentials(google.auth.credentials.Signing,
258 google.auth.credentials.Credentials):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700259 """Credentials that use a JWT as the bearer token.
260
261 These credentials require an "audience" claim. This claim identifies the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800262 intended recipient of the bearer token.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700263
264 The constructor arguments determine the claims for the JWT that is
265 sent with requests. Usually, you'll construct these credentials with
266 one of the helper constructors as shown in the next section.
267
268 To create JWT credentials using a Google service account private key
269 JSON file::
270
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800271 audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700272 credentials = jwt.Credentials.from_service_account_file(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800273 'service-account.json',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800274 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700275
276 If you already have the service account file loaded and parsed::
277
278 service_account_info = json.load(open('service_account.json'))
279 credentials = jwt.Credentials.from_service_account_info(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800280 service_account_info,
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800281 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700282
283 Both helper methods pass on arguments to the constructor, so you can
284 specify the JWT claims::
285
286 credentials = jwt.Credentials.from_service_account_file(
287 'service-account.json',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800288 audience=audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700289 additional_claims={'meta': 'data'})
290
291 You can also construct the credentials directly if you have a
292 :class:`~google.auth.crypt.Signer` instance::
293
294 credentials = jwt.Credentials(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800295 signer,
296 issuer='your-issuer',
297 subject='your-subject',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800298 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700299
300 The claims are considered immutable. If you want to modify the claims,
301 you can easily create another instance using :meth:`with_claims`::
302
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800303 new_audience = (
304 'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
305 new_credentials = credentials.with_claims(audience=new_audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700306 """
307
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800308 def __init__(self, signer, issuer, subject, audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700309 additional_claims=None,
310 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS):
311 """
312 Args:
313 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
314 issuer (str): The `iss` claim.
315 subject (str): The `sub` claim.
316 audience (str): the `aud` claim. The intended audience for the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800317 credentials.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700318 additional_claims (Mapping[str, str]): Any additional claims for
319 the JWT payload.
320 token_lifetime (int): The amount of time in seconds for
321 which the token is valid. Defaults to 1 hour.
322 """
323 super(Credentials, self).__init__()
324 self._signer = signer
325 self._issuer = issuer
326 self._subject = subject
327 self._audience = audience
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700328 self._token_lifetime = token_lifetime
329
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700330 if additional_claims is None:
331 additional_claims = {}
332
333 self._additional_claims = additional_claims
Danny Hermes93d1aa42016-10-17 13:15:07 -0700334
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700335 @classmethod
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700336 def _from_signer_and_info(cls, signer, info, **kwargs):
337 """Creates a Credentials instance from a signer and service account
338 info.
339
340 Args:
341 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
342 info (Mapping[str, str]): The service account info.
343 kwargs: Additional arguments to pass to the constructor.
344
345 Returns:
346 google.auth.jwt.Credentials: The constructed credentials.
347
348 Raises:
349 ValueError: If the info is not in the expected format.
350 """
351 kwargs.setdefault('subject', info['client_email'])
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800352 kwargs.setdefault('issuer', info['client_email'])
353 return cls(signer, **kwargs)
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700354
355 @classmethod
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700356 def from_service_account_info(cls, info, **kwargs):
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700357 """Creates an Credentials instance from a dictionary.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700358
359 Args:
360 info (Mapping[str, str]): The service account info in Google
361 format.
362 kwargs: Additional arguments to pass to the constructor.
363
364 Returns:
365 google.auth.jwt.Credentials: The constructed credentials.
366
367 Raises:
368 ValueError: If the info is not in the expected format.
369 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700370 signer = _service_account_info.from_dict(
371 info, require=['client_email'])
372 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700373
374 @classmethod
375 def from_service_account_file(cls, filename, **kwargs):
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700376 """Creates a Credentials instance from a service account .json file
377 in Google format.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700378
379 Args:
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700380 filename (str): The path to the service account .json file.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700381 kwargs: Additional arguments to pass to the constructor.
382
383 Returns:
384 google.auth.jwt.Credentials: The constructed credentials.
385 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700386 info, signer = _service_account_info.from_filename(
387 filename, require=['client_email'])
388 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700389
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800390 @classmethod
391 def from_signing_credentials(cls, credentials, audience, **kwargs):
392 """Creates a new :class:`google.auth.jwt.Credentials` instance from an
393 existing :class:`google.auth.credentials.Signing` instance.
394
395 The new instance will use the same signer as the existing instance and
396 will use the existing instance's signer email as the issuer and
397 subject by default.
398
399 Example::
400
401 svc_creds = service_account.Credentials.from_service_account_file(
402 'service_account.json')
403 audience = (
404 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher')
405 jwt_creds = jwt.Credentials.from_signing_credentials(
406 svc_creds, audience=audience)
407
408 Args:
409 credentials (google.auth.credentials.Signing): The credentials to
410 use to construct the new credentials.
411 audience (str): the `aud` claim. The intended audience for the
412 credentials.
413 kwargs: Additional arguments to pass to the constructor.
414
415 Returns:
416 google.auth.jwt.Credentials: A new Credentials instance.
417 """
418 kwargs.setdefault('issuer', credentials.signer_email)
419 kwargs.setdefault('subject', credentials.signer_email)
420 return cls(
421 credentials.signer,
422 audience=audience,
423 **kwargs)
424
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700425 def with_claims(self, issuer=None, subject=None, audience=None,
426 additional_claims=None):
427 """Returns a copy of these credentials with modified claims.
428
429 Args:
430 issuer (str): The `iss` claim. If unspecified the current issuer
431 claim will be used.
432 subject (str): The `sub` claim. If unspecified the current subject
433 claim will be used.
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800434 audience (str): the `aud` claim. If unspecified the current
435 audience claim will be used.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700436 additional_claims (Mapping[str, str]): Any additional claims for
437 the JWT payload. This will be merged with the current
438 additional claims.
439
440 Returns:
441 google.auth.jwt.Credentials: A new credentials instance.
442 """
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700443 new_additional_claims = copy.deepcopy(self._additional_claims)
444 new_additional_claims.update(additional_claims or {})
445
Christophe Tatonb649b432018-02-08 14:12:23 -0800446 return self.__class__(
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700447 self._signer,
448 issuer=issuer if issuer is not None else self._issuer,
449 subject=subject if subject is not None else self._subject,
450 audience=audience if audience is not None else self._audience,
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700451 additional_claims=new_additional_claims)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700452
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800453 def _make_jwt(self):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700454 """Make a signed JWT.
455
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700456 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800457 Tuple[bytes, datetime]: The encoded JWT and the expiration.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700458 """
459 now = _helpers.utcnow()
460 lifetime = datetime.timedelta(seconds=self._token_lifetime)
461 expiry = now + lifetime
462
463 payload = {
464 'iss': self._issuer,
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800465 'sub': self._subject,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700466 'iat': _helpers.datetime_to_secs(now),
467 'exp': _helpers.datetime_to_secs(expiry),
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800468 'aud': self._audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700469 }
470
471 payload.update(self._additional_claims)
472
473 jwt = encode(self._signer, payload)
474
475 return jwt, expiry
476
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700477 def refresh(self, request):
478 """Refreshes the access token.
479
480 Args:
481 request (Any): Unused.
482 """
483 # pylint: disable=unused-argument
484 # (pylint doesn't correctly recognize overridden methods.)
485 self.token, self.expiry = self._make_jwt()
486
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800487 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700488 def sign_bytes(self, message):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700489 return self._signer.sign(message)
490
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800491 @property
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800492 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800493 def signer_email(self):
494 return self._issuer
495
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800496 @property
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800497 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800498 def signer(self):
499 return self._signer
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700500
501
502class OnDemandCredentials(
503 google.auth.credentials.Signing,
504 google.auth.credentials.Credentials):
505 """On-demand JWT credentials.
506
507 Like :class:`Credentials`, this class uses a JWT as the bearer token for
508 authentication. However, this class does not require the audience at
509 construction time. Instead, it will generate a new token on-demand for
510 each request using the request URI as the audience. It caches tokens
511 so that multiple requests to the same URI do not incur the overhead
512 of generating a new token every time.
513
514 This behavior is especially useful for `gRPC`_ clients. A gRPC service may
515 have multiple audience and gRPC clients may not know all of the audiences
516 required for accessing a particular service. With these credentials,
517 no knowledge of the audiences is required ahead of time.
518
519 .. _grpc: http://www.grpc.io/
520 """
521
522 def __init__(self, signer, issuer, subject,
523 additional_claims=None,
524 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
525 max_cache_size=_DEFAULT_MAX_CACHE_SIZE):
526 """
527 Args:
528 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
529 issuer (str): The `iss` claim.
530 subject (str): The `sub` claim.
531 additional_claims (Mapping[str, str]): Any additional claims for
532 the JWT payload.
533 token_lifetime (int): The amount of time in seconds for
534 which the token is valid. Defaults to 1 hour.
535 max_cache_size (int): The maximum number of JWT tokens to keep in
536 cache. Tokens are cached using :class:`cachetools.LRUCache`.
537 """
538 super(OnDemandCredentials, self).__init__()
539 self._signer = signer
540 self._issuer = issuer
541 self._subject = subject
542 self._token_lifetime = token_lifetime
543
544 if additional_claims is None:
545 additional_claims = {}
546
547 self._additional_claims = additional_claims
548 self._cache = cachetools.LRUCache(maxsize=max_cache_size)
549
550 @classmethod
551 def _from_signer_and_info(cls, signer, info, **kwargs):
552 """Creates an OnDemandCredentials instance from a signer and service
553 account info.
554
555 Args:
556 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
557 info (Mapping[str, str]): The service account info.
558 kwargs: Additional arguments to pass to the constructor.
559
560 Returns:
561 google.auth.jwt.OnDemandCredentials: The constructed credentials.
562
563 Raises:
564 ValueError: If the info is not in the expected format.
565 """
566 kwargs.setdefault('subject', info['client_email'])
567 kwargs.setdefault('issuer', info['client_email'])
568 return cls(signer, **kwargs)
569
570 @classmethod
571 def from_service_account_info(cls, info, **kwargs):
572 """Creates an OnDemandCredentials instance from a dictionary.
573
574 Args:
575 info (Mapping[str, str]): The service account info in Google
576 format.
577 kwargs: Additional arguments to pass to the constructor.
578
579 Returns:
580 google.auth.jwt.OnDemandCredentials: The constructed credentials.
581
582 Raises:
583 ValueError: If the info is not in the expected format.
584 """
585 signer = _service_account_info.from_dict(
586 info, require=['client_email'])
587 return cls._from_signer_and_info(signer, info, **kwargs)
588
589 @classmethod
590 def from_service_account_file(cls, filename, **kwargs):
591 """Creates an OnDemandCredentials instance from a service account .json
592 file in Google format.
593
594 Args:
595 filename (str): The path to the service account .json file.
596 kwargs: Additional arguments to pass to the constructor.
597
598 Returns:
599 google.auth.jwt.OnDemandCredentials: The constructed credentials.
600 """
601 info, signer = _service_account_info.from_filename(
602 filename, require=['client_email'])
603 return cls._from_signer_and_info(signer, info, **kwargs)
604
605 @classmethod
606 def from_signing_credentials(cls, credentials, **kwargs):
607 """Creates a new :class:`google.auth.jwt.OnDemandCredentials` instance
608 from an existing :class:`google.auth.credentials.Signing` instance.
609
610 The new instance will use the same signer as the existing instance and
611 will use the existing instance's signer email as the issuer and
612 subject by default.
613
614 Example::
615
616 svc_creds = service_account.Credentials.from_service_account_file(
617 'service_account.json')
618 jwt_creds = jwt.OnDemandCredentials.from_signing_credentials(
619 svc_creds)
620
621 Args:
622 credentials (google.auth.credentials.Signing): The credentials to
623 use to construct the new credentials.
624 kwargs: Additional arguments to pass to the constructor.
625
626 Returns:
627 google.auth.jwt.Credentials: A new Credentials instance.
628 """
629 kwargs.setdefault('issuer', credentials.signer_email)
630 kwargs.setdefault('subject', credentials.signer_email)
631 return cls(credentials.signer, **kwargs)
632
633 def with_claims(self, issuer=None, subject=None, additional_claims=None):
634 """Returns a copy of these credentials with modified claims.
635
636 Args:
637 issuer (str): The `iss` claim. If unspecified the current issuer
638 claim will be used.
639 subject (str): The `sub` claim. If unspecified the current subject
640 claim will be used.
641 additional_claims (Mapping[str, str]): Any additional claims for
642 the JWT payload. This will be merged with the current
643 additional claims.
644
645 Returns:
646 google.auth.jwt.OnDemandCredentials: A new credentials instance.
647 """
648 new_additional_claims = copy.deepcopy(self._additional_claims)
649 new_additional_claims.update(additional_claims or {})
650
Christophe Tatonb649b432018-02-08 14:12:23 -0800651 return self.__class__(
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700652 self._signer,
653 issuer=issuer if issuer is not None else self._issuer,
654 subject=subject if subject is not None else self._subject,
655 additional_claims=new_additional_claims,
656 max_cache_size=self._cache.maxsize)
657
658 @property
659 def valid(self):
660 """Checks the validity of the credentials.
661
662 These credentials are always valid because it generates tokens on
663 demand.
664 """
665 return True
666
667 def _make_jwt_for_audience(self, audience):
668 """Make a new JWT for the given audience.
669
670 Args:
671 audience (str): The intended audience.
672
673 Returns:
674 Tuple[bytes, datetime]: The encoded JWT and the expiration.
675 """
676 now = _helpers.utcnow()
677 lifetime = datetime.timedelta(seconds=self._token_lifetime)
678 expiry = now + lifetime
679
680 payload = {
681 'iss': self._issuer,
682 'sub': self._subject,
683 'iat': _helpers.datetime_to_secs(now),
684 'exp': _helpers.datetime_to_secs(expiry),
685 'aud': audience,
686 }
687
688 payload.update(self._additional_claims)
689
690 jwt = encode(self._signer, payload)
691
692 return jwt, expiry
693
694 def _get_jwt_for_audience(self, audience):
695 """Get a JWT For a given audience.
696
697 If there is already an existing, non-expired token in the cache for
698 the audience, that token is used. Otherwise, a new token will be
699 created.
700
701 Args:
702 audience (str): The intended audience.
703
704 Returns:
705 bytes: The encoded JWT.
706 """
707 token, expiry = self._cache.get(audience, (None, None))
708
709 if token is None or expiry < _helpers.utcnow():
710 token, expiry = self._make_jwt_for_audience(audience)
711 self._cache[audience] = token, expiry
712
713 return token
714
715 def refresh(self, request):
716 """Raises an exception, these credentials can not be directly
717 refreshed.
718
719 Args:
720 request (Any): Unused.
721
722 Raises:
723 google.auth.RefreshError
724 """
725 # pylint: disable=unused-argument
726 # (pylint doesn't correctly recognize overridden methods.)
727 raise exceptions.RefreshError(
728 'OnDemandCredentials can not be directly refreshed.')
729
730 def before_request(self, request, method, url, headers):
731 """Performs credential-specific before request logic.
732
733 Args:
734 request (Any): Unused. JWT credentials do not need to make an
735 HTTP request to refresh.
736 method (str): The request's HTTP method.
737 url (str): The request's URI. This is used as the audience claim
738 when generating the JWT.
739 headers (Mapping): The request's headers.
740 """
741 # pylint: disable=unused-argument
742 # (pylint doesn't correctly recognize overridden methods.)
743 parts = urllib.parse.urlsplit(url)
744 # Strip query string and fragment
745 audience = urllib.parse.urlunsplit(
Teddy Sudola10b15e2018-10-05 10:20:33 -0700746 (parts.scheme, parts.netloc, parts.path, "", ""))
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700747 token = self._get_jwt_for_audience(audience)
748 self.apply(headers, token=token)
749
750 @_helpers.copy_docstring(google.auth.credentials.Signing)
751 def sign_bytes(self, message):
752 return self._signer.sign(message)
753
754 @property
755 @_helpers.copy_docstring(google.auth.credentials.Signing)
756 def signer_email(self):
757 return self._issuer
758
759 @property
760 @_helpers.copy_docstring(google.auth.credentials.Signing)
761 def signer(self):
762 return self._signer