blob: 361c4567ae7c79be3753b0ca570148dc2f81f33b [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
Jay Leec5a33952020-01-17 11:18:47 -080043try:
44 from collections.abc import Mapping
45except ImportError: # Python 2.7 compatibility
46 from collections import Mapping
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -070047import copy
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -070048import datetime
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070049import json
50
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070051import cachetools
Danny Hermes895e3692017-11-09 11:35:57 -080052import six
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070053from six.moves import urllib
54
Jon Wayne Parrott54a85172016-10-17 11:27:37 -070055from google.auth import _helpers
Jon Wayne Parrott807032c2016-10-18 09:38:26 -070056from google.auth import _service_account_info
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070057from google.auth import crypt
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070058from google.auth import exceptions
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -080059import google.auth.credentials
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070060
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -070061_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070062_DEFAULT_MAX_CACHE_SIZE = 10
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070063
64
65def encode(signer, payload, header=None, key_id=None):
66 """Make a signed JWT.
67
68 Args:
69 signer (google.auth.crypt.Signer): The signer used to sign the JWT.
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -070070 payload (Mapping[str, str]): The JWT payload.
71 header (Mapping[str, str]): Additional JWT header payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070072 key_id (str): The key id to add to the JWT header. If the
73 signer has a key id it will be used as the default. If this is
74 specified it will override the signer's key id.
75
76 Returns:
77 bytes: The encoded JWT.
78 """
79 if header is None:
80 header = {}
81
82 if key_id is None:
83 key_id = signer.key_id
84
Bu Sun Kim9eec0912019-10-21 17:04:21 -070085 header.update({"typ": "JWT", "alg": "RS256"})
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070086
87 if key_id is not None:
Bu Sun Kim9eec0912019-10-21 17:04:21 -070088 header["kid"] = key_id
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070089
90 segments = [
Bu Sun Kim9eec0912019-10-21 17:04:21 -070091 _helpers.unpadded_urlsafe_b64encode(json.dumps(header).encode("utf-8")),
92 _helpers.unpadded_urlsafe_b64encode(json.dumps(payload).encode("utf-8")),
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070093 ]
94
Bu Sun Kim9eec0912019-10-21 17:04:21 -070095 signing_input = b".".join(segments)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070096 signature = signer.sign(signing_input)
Bu Sun Kim9eec0912019-10-21 17:04:21 -070097 segments.append(_helpers.unpadded_urlsafe_b64encode(signature))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070098
Bu Sun Kim9eec0912019-10-21 17:04:21 -070099 return b".".join(segments)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700100
101
102def _decode_jwt_segment(encoded_section):
103 """Decodes a single JWT segment."""
Jon Wayne Parrott97eb8702016-11-17 09:43:16 -0800104 section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700105 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700106 return json.loads(section_bytes.decode("utf-8"))
Danny Hermes895e3692017-11-09 11:35:57 -0800107 except ValueError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700108 new_exc = ValueError("Can't parse segment: {0}".format(section_bytes))
Danny Hermes895e3692017-11-09 11:35:57 -0800109 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700110
111
112def _unverified_decode(token):
113 """Decodes a token and does no verification.
114
115 Args:
116 token (Union[str, bytes]): The encoded JWT.
117
118 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800119 Tuple[str, str, str, str]: header, payload, signed_section, and
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700120 signature.
121
122 Raises:
123 ValueError: if there are an incorrect amount of segments in the token.
124 """
125 token = _helpers.to_bytes(token)
126
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700127 if token.count(b".") != 2:
128 raise ValueError("Wrong number of segments in token: {0}".format(token))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700129
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700130 encoded_header, encoded_payload, signature = token.split(b".")
131 signed_section = encoded_header + b"." + encoded_payload
Jon Wayne Parrott97eb8702016-11-17 09:43:16 -0800132 signature = _helpers.padded_urlsafe_b64decode(signature)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700133
134 # Parse segments
135 header = _decode_jwt_segment(encoded_header)
136 payload = _decode_jwt_segment(encoded_payload)
137
138 return header, payload, signed_section, signature
139
140
141def decode_header(token):
142 """Return the decoded header of a token.
143
144 No verification is done. This is useful to extract the key id from
145 the header in order to acquire the appropriate certificate to verify
146 the token.
147
148 Args:
149 token (Union[str, bytes]): the encoded JWT.
150
151 Returns:
152 Mapping: The decoded JWT header.
153 """
154 header, _, _, _ = _unverified_decode(token)
155 return header
156
157
158def _verify_iat_and_exp(payload):
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700159 """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700160 payload.
161
162 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700163 payload (Mapping[str, str]): The JWT payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700164
165 Raises:
166 ValueError: if any checks failed.
167 """
168 now = _helpers.datetime_to_secs(_helpers.utcnow())
169
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700170 # Make sure the iat and exp claims are present.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700171 for key in ("iat", "exp"):
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700172 if key not in payload:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700173 raise ValueError("Token does not contain required claim {}".format(key))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700174
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700175 # Make sure the token wasn't issued in the future.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700176 iat = payload["iat"]
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700177 # Err on the side of accepting a token that is slightly early to account
178 # for clock skew.
179 earliest = iat - _helpers.CLOCK_SKEW_SECS
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700180 if now < earliest:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700181 raise ValueError("Token used too early, {} < {}".format(now, iat))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700182
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700183 # Make sure the token wasn't issued in the past.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700184 exp = payload["exp"]
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700185 # Err on the side of accepting a token that is slightly out of date
186 # to account for clow skew.
187 latest = exp + _helpers.CLOCK_SKEW_SECS
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700188 if latest < now:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700189 raise ValueError("Token expired, {} < {}".format(latest, now))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700190
191
192def decode(token, certs=None, verify=True, audience=None):
193 """Decode and verify a JWT.
194
195 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700196 token (str): The encoded JWT.
197 certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
Tianzi Cai2c6ad782019-03-29 13:49:06 -0700198 certificate used to validate the JWT signature. If bytes or string,
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700199 it must the the public key certificate in PEM format. If a mapping,
200 it must be a mapping of key IDs to public key certificates in PEM
201 format. The mapping must contain the same key ID that's specified
202 in the token's header.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700203 verify (bool): Whether to perform signature and claim validation.
204 Verification is done by default.
205 audience (str): The audience claim, 'aud', that this JWT should
206 contain. If None then the JWT's 'aud' parameter is not verified.
207
208 Returns:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700209 Mapping[str, str]: The deserialized JSON payload in the JWT.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700210
211 Raises:
212 ValueError: if any verification checks failed.
213 """
214 header, payload, signed_section, signature = _unverified_decode(token)
215
216 if not verify:
217 return payload
218
219 # If certs is specified as a dictionary of key IDs to certificates, then
220 # use the certificate identified by the key ID in the token header.
Jay Leec5a33952020-01-17 11:18:47 -0800221 if isinstance(certs, Mapping):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700222 key_id = header.get("kid")
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700223 if key_id:
224 if key_id not in certs:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700225 raise ValueError("Certificate for key id {} not found.".format(key_id))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700226 certs_to_check = [certs[key_id]]
227 # If there's no key id in the header, check against all of the certs.
228 else:
229 certs_to_check = certs.values()
230 else:
231 certs_to_check = certs
232
233 # Verify that the signature matches the message.
234 if not crypt.verify_signature(signed_section, signature, certs_to_check):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700235 raise ValueError("Could not verify token signature.")
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700236
237 # Verify the issued at and created times in the payload.
238 _verify_iat_and_exp(payload)
239
240 # Check audience.
241 if audience is not None:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700242 claim_audience = payload.get("aud")
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700243 if audience != claim_audience:
244 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700245 "Token has wrong audience {}, expected {}".format(
246 claim_audience, audience
247 )
248 )
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700249
250 return payload
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700251
252
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700253class Credentials(google.auth.credentials.Signing, google.auth.credentials.Credentials):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700254 """Credentials that use a JWT as the bearer token.
255
256 These credentials require an "audience" claim. This claim identifies the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800257 intended recipient of the bearer token.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700258
259 The constructor arguments determine the claims for the JWT that is
260 sent with requests. Usually, you'll construct these credentials with
261 one of the helper constructors as shown in the next section.
262
263 To create JWT credentials using a Google service account private key
264 JSON file::
265
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800266 audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700267 credentials = jwt.Credentials.from_service_account_file(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800268 'service-account.json',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800269 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700270
271 If you already have the service account file loaded and parsed::
272
273 service_account_info = json.load(open('service_account.json'))
274 credentials = jwt.Credentials.from_service_account_info(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800275 service_account_info,
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800276 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700277
278 Both helper methods pass on arguments to the constructor, so you can
279 specify the JWT claims::
280
281 credentials = jwt.Credentials.from_service_account_file(
282 'service-account.json',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800283 audience=audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700284 additional_claims={'meta': 'data'})
285
286 You can also construct the credentials directly if you have a
287 :class:`~google.auth.crypt.Signer` instance::
288
289 credentials = jwt.Credentials(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800290 signer,
291 issuer='your-issuer',
292 subject='your-subject',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800293 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700294
295 The claims are considered immutable. If you want to modify the claims,
296 you can easily create another instance using :meth:`with_claims`::
297
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800298 new_audience = (
299 'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
300 new_credentials = credentials.with_claims(audience=new_audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700301 """
302
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700303 def __init__(
304 self,
305 signer,
306 issuer,
307 subject,
308 audience,
309 additional_claims=None,
310 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
311 ):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700312 """
313 Args:
314 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
315 issuer (str): The `iss` claim.
316 subject (str): The `sub` claim.
317 audience (str): the `aud` claim. The intended audience for the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800318 credentials.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700319 additional_claims (Mapping[str, str]): Any additional claims for
320 the JWT payload.
321 token_lifetime (int): The amount of time in seconds for
322 which the token is valid. Defaults to 1 hour.
323 """
324 super(Credentials, self).__init__()
325 self._signer = signer
326 self._issuer = issuer
327 self._subject = subject
328 self._audience = audience
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700329 self._token_lifetime = token_lifetime
330
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700331 if additional_claims is None:
332 additional_claims = {}
333
334 self._additional_claims = additional_claims
Danny Hermes93d1aa42016-10-17 13:15:07 -0700335
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700336 @classmethod
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700337 def _from_signer_and_info(cls, signer, info, **kwargs):
338 """Creates a Credentials instance from a signer and service account
339 info.
340
341 Args:
342 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
343 info (Mapping[str, str]): The service account info.
344 kwargs: Additional arguments to pass to the constructor.
345
346 Returns:
347 google.auth.jwt.Credentials: The constructed credentials.
348
349 Raises:
350 ValueError: If the info is not in the expected format.
351 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700352 kwargs.setdefault("subject", info["client_email"])
353 kwargs.setdefault("issuer", info["client_email"])
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800354 return cls(signer, **kwargs)
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700355
356 @classmethod
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700357 def from_service_account_info(cls, info, **kwargs):
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700358 """Creates an Credentials instance from a dictionary.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700359
360 Args:
361 info (Mapping[str, str]): The service account info in Google
362 format.
363 kwargs: Additional arguments to pass to the constructor.
364
365 Returns:
366 google.auth.jwt.Credentials: The constructed credentials.
367
368 Raises:
369 ValueError: If the info is not in the expected format.
370 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700371 signer = _service_account_info.from_dict(info, require=["client_email"])
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700372 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(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700387 filename, require=["client_email"]
388 )
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700389 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700390
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800391 @classmethod
392 def from_signing_credentials(cls, credentials, audience, **kwargs):
393 """Creates a new :class:`google.auth.jwt.Credentials` instance from an
394 existing :class:`google.auth.credentials.Signing` instance.
395
396 The new instance will use the same signer as the existing instance and
397 will use the existing instance's signer email as the issuer and
398 subject by default.
399
400 Example::
401
402 svc_creds = service_account.Credentials.from_service_account_file(
403 'service_account.json')
404 audience = (
405 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher')
406 jwt_creds = jwt.Credentials.from_signing_credentials(
407 svc_creds, audience=audience)
408
409 Args:
410 credentials (google.auth.credentials.Signing): The credentials to
411 use to construct the new credentials.
412 audience (str): the `aud` claim. The intended audience for the
413 credentials.
414 kwargs: Additional arguments to pass to the constructor.
415
416 Returns:
417 google.auth.jwt.Credentials: A new Credentials instance.
418 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700419 kwargs.setdefault("issuer", credentials.signer_email)
420 kwargs.setdefault("subject", credentials.signer_email)
421 return cls(credentials.signer, audience=audience, **kwargs)
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800422
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700423 def with_claims(
424 self, issuer=None, subject=None, audience=None, additional_claims=None
425 ):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700426 """Returns a copy of these credentials with modified claims.
427
428 Args:
429 issuer (str): The `iss` claim. If unspecified the current issuer
430 claim will be used.
431 subject (str): The `sub` claim. If unspecified the current subject
432 claim will be used.
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800433 audience (str): the `aud` claim. If unspecified the current
434 audience claim will be used.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700435 additional_claims (Mapping[str, str]): Any additional claims for
436 the JWT payload. This will be merged with the current
437 additional claims.
438
439 Returns:
440 google.auth.jwt.Credentials: A new credentials instance.
441 """
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700442 new_additional_claims = copy.deepcopy(self._additional_claims)
443 new_additional_claims.update(additional_claims or {})
444
Christophe Tatonb649b432018-02-08 14:12:23 -0800445 return self.__class__(
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700446 self._signer,
447 issuer=issuer if issuer is not None else self._issuer,
448 subject=subject if subject is not None else self._subject,
449 audience=audience if audience is not None else self._audience,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700450 additional_claims=new_additional_claims,
451 )
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 = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700464 "iss": self._issuer,
465 "sub": self._subject,
466 "iat": _helpers.datetime_to_secs(now),
467 "exp": _helpers.datetime_to_secs(expiry),
468 "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(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700503 google.auth.credentials.Signing, google.auth.credentials.Credentials
504):
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700505 """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
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700522 def __init__(
523 self,
524 signer,
525 issuer,
526 subject,
527 additional_claims=None,
528 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
529 max_cache_size=_DEFAULT_MAX_CACHE_SIZE,
530 ):
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700531 """
532 Args:
533 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
534 issuer (str): The `iss` claim.
535 subject (str): The `sub` claim.
536 additional_claims (Mapping[str, str]): Any additional claims for
537 the JWT payload.
538 token_lifetime (int): The amount of time in seconds for
539 which the token is valid. Defaults to 1 hour.
540 max_cache_size (int): The maximum number of JWT tokens to keep in
541 cache. Tokens are cached using :class:`cachetools.LRUCache`.
542 """
543 super(OnDemandCredentials, self).__init__()
544 self._signer = signer
545 self._issuer = issuer
546 self._subject = subject
547 self._token_lifetime = token_lifetime
548
549 if additional_claims is None:
550 additional_claims = {}
551
552 self._additional_claims = additional_claims
553 self._cache = cachetools.LRUCache(maxsize=max_cache_size)
554
555 @classmethod
556 def _from_signer_and_info(cls, signer, info, **kwargs):
557 """Creates an OnDemandCredentials instance from a signer and service
558 account info.
559
560 Args:
561 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
562 info (Mapping[str, str]): The service account info.
563 kwargs: Additional arguments to pass to the constructor.
564
565 Returns:
566 google.auth.jwt.OnDemandCredentials: The constructed credentials.
567
568 Raises:
569 ValueError: If the info is not in the expected format.
570 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700571 kwargs.setdefault("subject", info["client_email"])
572 kwargs.setdefault("issuer", info["client_email"])
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700573 return cls(signer, **kwargs)
574
575 @classmethod
576 def from_service_account_info(cls, info, **kwargs):
577 """Creates an OnDemandCredentials instance from a dictionary.
578
579 Args:
580 info (Mapping[str, str]): The service account info in Google
581 format.
582 kwargs: Additional arguments to pass to the constructor.
583
584 Returns:
585 google.auth.jwt.OnDemandCredentials: The constructed credentials.
586
587 Raises:
588 ValueError: If the info is not in the expected format.
589 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700590 signer = _service_account_info.from_dict(info, require=["client_email"])
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700591 return cls._from_signer_and_info(signer, info, **kwargs)
592
593 @classmethod
594 def from_service_account_file(cls, filename, **kwargs):
595 """Creates an OnDemandCredentials instance from a service account .json
596 file in Google format.
597
598 Args:
599 filename (str): The path to the service account .json file.
600 kwargs: Additional arguments to pass to the constructor.
601
602 Returns:
603 google.auth.jwt.OnDemandCredentials: The constructed credentials.
604 """
605 info, signer = _service_account_info.from_filename(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700606 filename, require=["client_email"]
607 )
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700608 return cls._from_signer_and_info(signer, info, **kwargs)
609
610 @classmethod
611 def from_signing_credentials(cls, credentials, **kwargs):
612 """Creates a new :class:`google.auth.jwt.OnDemandCredentials` instance
613 from an existing :class:`google.auth.credentials.Signing` instance.
614
615 The new instance will use the same signer as the existing instance and
616 will use the existing instance's signer email as the issuer and
617 subject by default.
618
619 Example::
620
621 svc_creds = service_account.Credentials.from_service_account_file(
622 'service_account.json')
623 jwt_creds = jwt.OnDemandCredentials.from_signing_credentials(
624 svc_creds)
625
626 Args:
627 credentials (google.auth.credentials.Signing): The credentials to
628 use to construct the new credentials.
629 kwargs: Additional arguments to pass to the constructor.
630
631 Returns:
632 google.auth.jwt.Credentials: A new Credentials instance.
633 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700634 kwargs.setdefault("issuer", credentials.signer_email)
635 kwargs.setdefault("subject", credentials.signer_email)
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700636 return cls(credentials.signer, **kwargs)
637
638 def with_claims(self, issuer=None, subject=None, additional_claims=None):
639 """Returns a copy of these credentials with modified claims.
640
641 Args:
642 issuer (str): The `iss` claim. If unspecified the current issuer
643 claim will be used.
644 subject (str): The `sub` claim. If unspecified the current subject
645 claim will be used.
646 additional_claims (Mapping[str, str]): Any additional claims for
647 the JWT payload. This will be merged with the current
648 additional claims.
649
650 Returns:
651 google.auth.jwt.OnDemandCredentials: A new credentials instance.
652 """
653 new_additional_claims = copy.deepcopy(self._additional_claims)
654 new_additional_claims.update(additional_claims or {})
655
Christophe Tatonb649b432018-02-08 14:12:23 -0800656 return self.__class__(
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700657 self._signer,
658 issuer=issuer if issuer is not None else self._issuer,
659 subject=subject if subject is not None else self._subject,
660 additional_claims=new_additional_claims,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700661 max_cache_size=self._cache.maxsize,
662 )
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700663
664 @property
665 def valid(self):
666 """Checks the validity of the credentials.
667
668 These credentials are always valid because it generates tokens on
669 demand.
670 """
671 return True
672
673 def _make_jwt_for_audience(self, audience):
674 """Make a new JWT for the given audience.
675
676 Args:
677 audience (str): The intended audience.
678
679 Returns:
680 Tuple[bytes, datetime]: The encoded JWT and the expiration.
681 """
682 now = _helpers.utcnow()
683 lifetime = datetime.timedelta(seconds=self._token_lifetime)
684 expiry = now + lifetime
685
686 payload = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700687 "iss": self._issuer,
688 "sub": self._subject,
689 "iat": _helpers.datetime_to_secs(now),
690 "exp": _helpers.datetime_to_secs(expiry),
691 "aud": audience,
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700692 }
693
694 payload.update(self._additional_claims)
695
696 jwt = encode(self._signer, payload)
697
698 return jwt, expiry
699
700 def _get_jwt_for_audience(self, audience):
701 """Get a JWT For a given audience.
702
703 If there is already an existing, non-expired token in the cache for
704 the audience, that token is used. Otherwise, a new token will be
705 created.
706
707 Args:
708 audience (str): The intended audience.
709
710 Returns:
711 bytes: The encoded JWT.
712 """
713 token, expiry = self._cache.get(audience, (None, None))
714
715 if token is None or expiry < _helpers.utcnow():
716 token, expiry = self._make_jwt_for_audience(audience)
717 self._cache[audience] = token, expiry
718
719 return token
720
721 def refresh(self, request):
722 """Raises an exception, these credentials can not be directly
723 refreshed.
724
725 Args:
726 request (Any): Unused.
727
728 Raises:
729 google.auth.RefreshError
730 """
731 # pylint: disable=unused-argument
732 # (pylint doesn't correctly recognize overridden methods.)
733 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700734 "OnDemandCredentials can not be directly refreshed."
735 )
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700736
737 def before_request(self, request, method, url, headers):
738 """Performs credential-specific before request logic.
739
740 Args:
741 request (Any): Unused. JWT credentials do not need to make an
742 HTTP request to refresh.
743 method (str): The request's HTTP method.
744 url (str): The request's URI. This is used as the audience claim
745 when generating the JWT.
746 headers (Mapping): The request's headers.
747 """
748 # pylint: disable=unused-argument
749 # (pylint doesn't correctly recognize overridden methods.)
750 parts = urllib.parse.urlsplit(url)
751 # Strip query string and fragment
752 audience = urllib.parse.urlunsplit(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700753 (parts.scheme, parts.netloc, parts.path, "", "")
754 )
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700755 token = self._get_jwt_for_audience(audience)
756 self.apply(headers, token=token)
757
758 @_helpers.copy_docstring(google.auth.credentials.Signing)
759 def sign_bytes(self, message):
760 return self._signer.sign(message)
761
762 @property
763 @_helpers.copy_docstring(google.auth.credentials.Signing)
764 def signer_email(self):
765 return self._issuer
766
767 @property
768 @_helpers.copy_docstring(google.auth.credentials.Signing)
769 def signer(self):
770 return self._signer