blob: cdd69ac8a398ed7e874cbde0f9fbd61923f65bcb [file] [log] [blame]
C.J. Collier37141e42020-02-13 13:49:49 -08001# Copyright 2016 Google LLC
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -07002#
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
Bu Sun Kim0ca0ee52020-01-18 00:38:49 -080045# Python 2.7 compatibility
46except ImportError: # pragma: NO COVER
Jay Leec5a33952020-01-17 11:18:47 -080047 from collections import Mapping
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -070048import copy
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -070049import datetime
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070050import json
51
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070052import cachetools
Danny Hermes895e3692017-11-09 11:35:57 -080053import six
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070054from six.moves import urllib
55
Jon Wayne Parrott54a85172016-10-17 11:27:37 -070056from google.auth import _helpers
Jon Wayne Parrott807032c2016-10-18 09:38:26 -070057from google.auth import _service_account_info
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070058from google.auth import crypt
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070059from google.auth import exceptions
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -080060import google.auth.credentials
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070061
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -070062_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -070063_DEFAULT_MAX_CACHE_SIZE = 10
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070064
65
66def encode(signer, payload, header=None, key_id=None):
67 """Make a signed JWT.
68
69 Args:
70 signer (google.auth.crypt.Signer): The signer used to sign the JWT.
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -070071 payload (Mapping[str, str]): The JWT payload.
72 header (Mapping[str, str]): Additional JWT header payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070073 key_id (str): The key id to add to the JWT header. If the
74 signer has a key id it will be used as the default. If this is
75 specified it will override the signer's key id.
76
77 Returns:
78 bytes: The encoded JWT.
79 """
80 if header is None:
81 header = {}
82
83 if key_id is None:
84 key_id = signer.key_id
85
Bu Sun Kim9eec0912019-10-21 17:04:21 -070086 header.update({"typ": "JWT", "alg": "RS256"})
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070087
88 if key_id is not None:
Bu Sun Kim9eec0912019-10-21 17:04:21 -070089 header["kid"] = key_id
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070090
91 segments = [
Bu Sun Kim9eec0912019-10-21 17:04:21 -070092 _helpers.unpadded_urlsafe_b64encode(json.dumps(header).encode("utf-8")),
93 _helpers.unpadded_urlsafe_b64encode(json.dumps(payload).encode("utf-8")),
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070094 ]
95
Bu Sun Kim9eec0912019-10-21 17:04:21 -070096 signing_input = b".".join(segments)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070097 signature = signer.sign(signing_input)
Bu Sun Kim9eec0912019-10-21 17:04:21 -070098 segments.append(_helpers.unpadded_urlsafe_b64encode(signature))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070099
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700100 return b".".join(segments)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700101
102
103def _decode_jwt_segment(encoded_section):
104 """Decodes a single JWT segment."""
Jon Wayne Parrott97eb8702016-11-17 09:43:16 -0800105 section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700106 try:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700107 return json.loads(section_bytes.decode("utf-8"))
Danny Hermes895e3692017-11-09 11:35:57 -0800108 except ValueError as caught_exc:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700109 new_exc = ValueError("Can't parse segment: {0}".format(section_bytes))
Danny Hermes895e3692017-11-09 11:35:57 -0800110 six.raise_from(new_exc, caught_exc)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700111
112
113def _unverified_decode(token):
114 """Decodes a token and does no verification.
115
116 Args:
117 token (Union[str, bytes]): The encoded JWT.
118
119 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800120 Tuple[str, str, str, str]: header, payload, signed_section, and
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700121 signature.
122
123 Raises:
124 ValueError: if there are an incorrect amount of segments in the token.
125 """
126 token = _helpers.to_bytes(token)
127
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700128 if token.count(b".") != 2:
129 raise ValueError("Wrong number of segments in token: {0}".format(token))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700130
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700131 encoded_header, encoded_payload, signature = token.split(b".")
132 signed_section = encoded_header + b"." + encoded_payload
Jon Wayne Parrott97eb8702016-11-17 09:43:16 -0800133 signature = _helpers.padded_urlsafe_b64decode(signature)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700134
135 # Parse segments
136 header = _decode_jwt_segment(encoded_header)
137 payload = _decode_jwt_segment(encoded_payload)
138
139 return header, payload, signed_section, signature
140
141
142def decode_header(token):
143 """Return the decoded header of a token.
144
145 No verification is done. This is useful to extract the key id from
146 the header in order to acquire the appropriate certificate to verify
147 the token.
148
149 Args:
150 token (Union[str, bytes]): the encoded JWT.
151
152 Returns:
153 Mapping: The decoded JWT header.
154 """
155 header, _, _, _ = _unverified_decode(token)
156 return header
157
158
159def _verify_iat_and_exp(payload):
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700160 """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700161 payload.
162
163 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700164 payload (Mapping[str, str]): The JWT payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700165
166 Raises:
167 ValueError: if any checks failed.
168 """
169 now = _helpers.datetime_to_secs(_helpers.utcnow())
170
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700171 # Make sure the iat and exp claims are present.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700172 for key in ("iat", "exp"):
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700173 if key not in payload:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700174 raise ValueError("Token does not contain required claim {}".format(key))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700175
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700176 # Make sure the token wasn't issued in the future.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700177 iat = payload["iat"]
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700178 # Err on the side of accepting a token that is slightly early to account
179 # for clock skew.
180 earliest = iat - _helpers.CLOCK_SKEW_SECS
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700181 if now < earliest:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700182 raise ValueError("Token used too early, {} < {}".format(now, iat))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700183
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700184 # Make sure the token wasn't issued in the past.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700185 exp = payload["exp"]
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700186 # Err on the side of accepting a token that is slightly out of date
187 # to account for clow skew.
188 latest = exp + _helpers.CLOCK_SKEW_SECS
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700189 if latest < now:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700190 raise ValueError("Token expired, {} < {}".format(latest, now))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700191
192
193def decode(token, certs=None, verify=True, audience=None):
194 """Decode and verify a JWT.
195
196 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700197 token (str): The encoded JWT.
198 certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
Tianzi Cai2c6ad782019-03-29 13:49:06 -0700199 certificate used to validate the JWT signature. If bytes or string,
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700200 it must the the public key certificate in PEM format. If a mapping,
201 it must be a mapping of key IDs to public key certificates in PEM
202 format. The mapping must contain the same key ID that's specified
203 in the token's header.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700204 verify (bool): Whether to perform signature and claim validation.
205 Verification is done by default.
206 audience (str): The audience claim, 'aud', that this JWT should
207 contain. If None then the JWT's 'aud' parameter is not verified.
208
209 Returns:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700210 Mapping[str, str]: The deserialized JSON payload in the JWT.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700211
212 Raises:
213 ValueError: if any verification checks failed.
214 """
215 header, payload, signed_section, signature = _unverified_decode(token)
216
217 if not verify:
218 return payload
219
220 # If certs is specified as a dictionary of key IDs to certificates, then
221 # use the certificate identified by the key ID in the token header.
Jay Leec5a33952020-01-17 11:18:47 -0800222 if isinstance(certs, Mapping):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700223 key_id = header.get("kid")
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700224 if key_id:
225 if key_id not in certs:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700226 raise ValueError("Certificate for key id {} not found.".format(key_id))
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700227 certs_to_check = [certs[key_id]]
228 # If there's no key id in the header, check against all of the certs.
229 else:
230 certs_to_check = certs.values()
231 else:
232 certs_to_check = certs
233
234 # Verify that the signature matches the message.
235 if not crypt.verify_signature(signed_section, signature, certs_to_check):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700236 raise ValueError("Could not verify token signature.")
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700237
238 # Verify the issued at and created times in the payload.
239 _verify_iat_and_exp(payload)
240
241 # Check audience.
242 if audience is not None:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700243 claim_audience = payload.get("aud")
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700244 if audience != claim_audience:
245 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700246 "Token has wrong audience {}, expected {}".format(
247 claim_audience, audience
248 )
249 )
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700250
251 return payload
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700252
253
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700254class Credentials(google.auth.credentials.Signing, google.auth.credentials.Credentials):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700255 """Credentials that use a JWT as the bearer token.
256
257 These credentials require an "audience" claim. This claim identifies the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800258 intended recipient of the bearer token.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700259
260 The constructor arguments determine the claims for the JWT that is
261 sent with requests. Usually, you'll construct these credentials with
262 one of the helper constructors as shown in the next section.
263
264 To create JWT credentials using a Google service account private key
265 JSON file::
266
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800267 audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700268 credentials = jwt.Credentials.from_service_account_file(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800269 'service-account.json',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800270 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700271
272 If you already have the service account file loaded and parsed::
273
274 service_account_info = json.load(open('service_account.json'))
275 credentials = jwt.Credentials.from_service_account_info(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800276 service_account_info,
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800277 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700278
279 Both helper methods pass on arguments to the constructor, so you can
280 specify the JWT claims::
281
282 credentials = jwt.Credentials.from_service_account_file(
283 'service-account.json',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800284 audience=audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700285 additional_claims={'meta': 'data'})
286
287 You can also construct the credentials directly if you have a
288 :class:`~google.auth.crypt.Signer` instance::
289
290 credentials = jwt.Credentials(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800291 signer,
292 issuer='your-issuer',
293 subject='your-subject',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800294 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700295
296 The claims are considered immutable. If you want to modify the claims,
297 you can easily create another instance using :meth:`with_claims`::
298
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800299 new_audience = (
300 'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
301 new_credentials = credentials.with_claims(audience=new_audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700302 """
303
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700304 def __init__(
305 self,
306 signer,
307 issuer,
308 subject,
309 audience,
310 additional_claims=None,
311 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
312 ):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700313 """
314 Args:
315 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
316 issuer (str): The `iss` claim.
317 subject (str): The `sub` claim.
318 audience (str): the `aud` claim. The intended audience for the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800319 credentials.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700320 additional_claims (Mapping[str, str]): Any additional claims for
321 the JWT payload.
322 token_lifetime (int): The amount of time in seconds for
323 which the token is valid. Defaults to 1 hour.
324 """
325 super(Credentials, self).__init__()
326 self._signer = signer
327 self._issuer = issuer
328 self._subject = subject
329 self._audience = audience
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700330 self._token_lifetime = token_lifetime
331
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700332 if additional_claims is None:
333 additional_claims = {}
334
335 self._additional_claims = additional_claims
Danny Hermes93d1aa42016-10-17 13:15:07 -0700336
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700337 @classmethod
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700338 def _from_signer_and_info(cls, signer, info, **kwargs):
339 """Creates a Credentials instance from a signer and service account
340 info.
341
342 Args:
343 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
344 info (Mapping[str, str]): The service account info.
345 kwargs: Additional arguments to pass to the constructor.
346
347 Returns:
348 google.auth.jwt.Credentials: The constructed credentials.
349
350 Raises:
351 ValueError: If the info is not in the expected format.
352 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700353 kwargs.setdefault("subject", info["client_email"])
354 kwargs.setdefault("issuer", info["client_email"])
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800355 return cls(signer, **kwargs)
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700356
357 @classmethod
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700358 def from_service_account_info(cls, info, **kwargs):
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700359 """Creates an Credentials instance from a dictionary.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700360
361 Args:
362 info (Mapping[str, str]): The service account info in Google
363 format.
364 kwargs: Additional arguments to pass to the constructor.
365
366 Returns:
367 google.auth.jwt.Credentials: The constructed credentials.
368
369 Raises:
370 ValueError: If the info is not in the expected format.
371 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700372 signer = _service_account_info.from_dict(info, require=["client_email"])
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700373 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700374
375 @classmethod
376 def from_service_account_file(cls, filename, **kwargs):
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700377 """Creates a Credentials instance from a service account .json file
378 in Google format.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700379
380 Args:
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700381 filename (str): The path to the service account .json file.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700382 kwargs: Additional arguments to pass to the constructor.
383
384 Returns:
385 google.auth.jwt.Credentials: The constructed credentials.
386 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700387 info, signer = _service_account_info.from_filename(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700388 filename, require=["client_email"]
389 )
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700390 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700391
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800392 @classmethod
393 def from_signing_credentials(cls, credentials, audience, **kwargs):
394 """Creates a new :class:`google.auth.jwt.Credentials` instance from an
395 existing :class:`google.auth.credentials.Signing` instance.
396
397 The new instance will use the same signer as the existing instance and
398 will use the existing instance's signer email as the issuer and
399 subject by default.
400
401 Example::
402
403 svc_creds = service_account.Credentials.from_service_account_file(
404 'service_account.json')
405 audience = (
406 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher')
407 jwt_creds = jwt.Credentials.from_signing_credentials(
408 svc_creds, audience=audience)
409
410 Args:
411 credentials (google.auth.credentials.Signing): The credentials to
412 use to construct the new credentials.
413 audience (str): the `aud` claim. The intended audience for the
414 credentials.
415 kwargs: Additional arguments to pass to the constructor.
416
417 Returns:
418 google.auth.jwt.Credentials: A new Credentials instance.
419 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700420 kwargs.setdefault("issuer", credentials.signer_email)
421 kwargs.setdefault("subject", credentials.signer_email)
422 return cls(credentials.signer, audience=audience, **kwargs)
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800423
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700424 def with_claims(
425 self, issuer=None, subject=None, audience=None, additional_claims=None
426 ):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700427 """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,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700451 additional_claims=new_additional_claims,
452 )
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700453
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800454 def _make_jwt(self):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700455 """Make a signed JWT.
456
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700457 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800458 Tuple[bytes, datetime]: The encoded JWT and the expiration.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700459 """
460 now = _helpers.utcnow()
461 lifetime = datetime.timedelta(seconds=self._token_lifetime)
462 expiry = now + lifetime
463
464 payload = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700465 "iss": self._issuer,
466 "sub": self._subject,
467 "iat": _helpers.datetime_to_secs(now),
468 "exp": _helpers.datetime_to_secs(expiry),
469 "aud": self._audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700470 }
471
472 payload.update(self._additional_claims)
473
474 jwt = encode(self._signer, payload)
475
476 return jwt, expiry
477
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700478 def refresh(self, request):
479 """Refreshes the access token.
480
481 Args:
482 request (Any): Unused.
483 """
484 # pylint: disable=unused-argument
485 # (pylint doesn't correctly recognize overridden methods.)
486 self.token, self.expiry = self._make_jwt()
487
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800488 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700489 def sign_bytes(self, message):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700490 return self._signer.sign(message)
491
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800492 @property
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800493 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800494 def signer_email(self):
495 return self._issuer
496
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800497 @property
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800498 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800499 def signer(self):
500 return self._signer
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700501
502
503class OnDemandCredentials(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700504 google.auth.credentials.Signing, google.auth.credentials.Credentials
505):
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700506 """On-demand JWT credentials.
507
508 Like :class:`Credentials`, this class uses a JWT as the bearer token for
509 authentication. However, this class does not require the audience at
510 construction time. Instead, it will generate a new token on-demand for
511 each request using the request URI as the audience. It caches tokens
512 so that multiple requests to the same URI do not incur the overhead
513 of generating a new token every time.
514
515 This behavior is especially useful for `gRPC`_ clients. A gRPC service may
516 have multiple audience and gRPC clients may not know all of the audiences
517 required for accessing a particular service. With these credentials,
518 no knowledge of the audiences is required ahead of time.
519
520 .. _grpc: http://www.grpc.io/
521 """
522
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700523 def __init__(
524 self,
525 signer,
526 issuer,
527 subject,
528 additional_claims=None,
529 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
530 max_cache_size=_DEFAULT_MAX_CACHE_SIZE,
531 ):
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700532 """
533 Args:
534 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
535 issuer (str): The `iss` claim.
536 subject (str): The `sub` claim.
537 additional_claims (Mapping[str, str]): Any additional claims for
538 the JWT payload.
539 token_lifetime (int): The amount of time in seconds for
540 which the token is valid. Defaults to 1 hour.
541 max_cache_size (int): The maximum number of JWT tokens to keep in
542 cache. Tokens are cached using :class:`cachetools.LRUCache`.
543 """
544 super(OnDemandCredentials, self).__init__()
545 self._signer = signer
546 self._issuer = issuer
547 self._subject = subject
548 self._token_lifetime = token_lifetime
549
550 if additional_claims is None:
551 additional_claims = {}
552
553 self._additional_claims = additional_claims
554 self._cache = cachetools.LRUCache(maxsize=max_cache_size)
555
556 @classmethod
557 def _from_signer_and_info(cls, signer, info, **kwargs):
558 """Creates an OnDemandCredentials instance from a signer and service
559 account info.
560
561 Args:
562 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
563 info (Mapping[str, str]): The service account info.
564 kwargs: Additional arguments to pass to the constructor.
565
566 Returns:
567 google.auth.jwt.OnDemandCredentials: The constructed credentials.
568
569 Raises:
570 ValueError: If the info is not in the expected format.
571 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700572 kwargs.setdefault("subject", info["client_email"])
573 kwargs.setdefault("issuer", info["client_email"])
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700574 return cls(signer, **kwargs)
575
576 @classmethod
577 def from_service_account_info(cls, info, **kwargs):
578 """Creates an OnDemandCredentials instance from a dictionary.
579
580 Args:
581 info (Mapping[str, str]): The service account info in Google
582 format.
583 kwargs: Additional arguments to pass to the constructor.
584
585 Returns:
586 google.auth.jwt.OnDemandCredentials: The constructed credentials.
587
588 Raises:
589 ValueError: If the info is not in the expected format.
590 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700591 signer = _service_account_info.from_dict(info, require=["client_email"])
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700592 return cls._from_signer_and_info(signer, info, **kwargs)
593
594 @classmethod
595 def from_service_account_file(cls, filename, **kwargs):
596 """Creates an OnDemandCredentials instance from a service account .json
597 file in Google format.
598
599 Args:
600 filename (str): The path to the service account .json file.
601 kwargs: Additional arguments to pass to the constructor.
602
603 Returns:
604 google.auth.jwt.OnDemandCredentials: The constructed credentials.
605 """
606 info, signer = _service_account_info.from_filename(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700607 filename, require=["client_email"]
608 )
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700609 return cls._from_signer_and_info(signer, info, **kwargs)
610
611 @classmethod
612 def from_signing_credentials(cls, credentials, **kwargs):
613 """Creates a new :class:`google.auth.jwt.OnDemandCredentials` instance
614 from an existing :class:`google.auth.credentials.Signing` instance.
615
616 The new instance will use the same signer as the existing instance and
617 will use the existing instance's signer email as the issuer and
618 subject by default.
619
620 Example::
621
622 svc_creds = service_account.Credentials.from_service_account_file(
623 'service_account.json')
624 jwt_creds = jwt.OnDemandCredentials.from_signing_credentials(
625 svc_creds)
626
627 Args:
628 credentials (google.auth.credentials.Signing): The credentials to
629 use to construct the new credentials.
630 kwargs: Additional arguments to pass to the constructor.
631
632 Returns:
633 google.auth.jwt.Credentials: A new Credentials instance.
634 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700635 kwargs.setdefault("issuer", credentials.signer_email)
636 kwargs.setdefault("subject", credentials.signer_email)
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700637 return cls(credentials.signer, **kwargs)
638
639 def with_claims(self, issuer=None, subject=None, additional_claims=None):
640 """Returns a copy of these credentials with modified claims.
641
642 Args:
643 issuer (str): The `iss` claim. If unspecified the current issuer
644 claim will be used.
645 subject (str): The `sub` claim. If unspecified the current subject
646 claim will be used.
647 additional_claims (Mapping[str, str]): Any additional claims for
648 the JWT payload. This will be merged with the current
649 additional claims.
650
651 Returns:
652 google.auth.jwt.OnDemandCredentials: A new credentials instance.
653 """
654 new_additional_claims = copy.deepcopy(self._additional_claims)
655 new_additional_claims.update(additional_claims or {})
656
Christophe Tatonb649b432018-02-08 14:12:23 -0800657 return self.__class__(
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700658 self._signer,
659 issuer=issuer if issuer is not None else self._issuer,
660 subject=subject if subject is not None else self._subject,
661 additional_claims=new_additional_claims,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700662 max_cache_size=self._cache.maxsize,
663 )
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700664
665 @property
666 def valid(self):
667 """Checks the validity of the credentials.
668
669 These credentials are always valid because it generates tokens on
670 demand.
671 """
672 return True
673
674 def _make_jwt_for_audience(self, audience):
675 """Make a new JWT for the given audience.
676
677 Args:
678 audience (str): The intended audience.
679
680 Returns:
681 Tuple[bytes, datetime]: The encoded JWT and the expiration.
682 """
683 now = _helpers.utcnow()
684 lifetime = datetime.timedelta(seconds=self._token_lifetime)
685 expiry = now + lifetime
686
687 payload = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700688 "iss": self._issuer,
689 "sub": self._subject,
690 "iat": _helpers.datetime_to_secs(now),
691 "exp": _helpers.datetime_to_secs(expiry),
692 "aud": audience,
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700693 }
694
695 payload.update(self._additional_claims)
696
697 jwt = encode(self._signer, payload)
698
699 return jwt, expiry
700
701 def _get_jwt_for_audience(self, audience):
702 """Get a JWT For a given audience.
703
704 If there is already an existing, non-expired token in the cache for
705 the audience, that token is used. Otherwise, a new token will be
706 created.
707
708 Args:
709 audience (str): The intended audience.
710
711 Returns:
712 bytes: The encoded JWT.
713 """
714 token, expiry = self._cache.get(audience, (None, None))
715
716 if token is None or expiry < _helpers.utcnow():
717 token, expiry = self._make_jwt_for_audience(audience)
718 self._cache[audience] = token, expiry
719
720 return token
721
722 def refresh(self, request):
723 """Raises an exception, these credentials can not be directly
724 refreshed.
725
726 Args:
727 request (Any): Unused.
728
729 Raises:
730 google.auth.RefreshError
731 """
732 # pylint: disable=unused-argument
733 # (pylint doesn't correctly recognize overridden methods.)
734 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700735 "OnDemandCredentials can not be directly refreshed."
736 )
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700737
738 def before_request(self, request, method, url, headers):
739 """Performs credential-specific before request logic.
740
741 Args:
742 request (Any): Unused. JWT credentials do not need to make an
743 HTTP request to refresh.
744 method (str): The request's HTTP method.
745 url (str): The request's URI. This is used as the audience claim
746 when generating the JWT.
747 headers (Mapping): The request's headers.
748 """
749 # pylint: disable=unused-argument
750 # (pylint doesn't correctly recognize overridden methods.)
751 parts = urllib.parse.urlsplit(url)
752 # Strip query string and fragment
753 audience = urllib.parse.urlunsplit(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700754 (parts.scheme, parts.netloc, parts.path, "", "")
755 )
Jon Wayne Parrottcfbfd252017-03-28 13:03:11 -0700756 token = self._get_jwt_for_audience(audience)
757 self.apply(headers, token=token)
758
759 @_helpers.copy_docstring(google.auth.credentials.Signing)
760 def sign_bytes(self, message):
761 return self._signer.sign(message)
762
763 @property
764 @_helpers.copy_docstring(google.auth.credentials.Signing)
765 def signer_email(self):
766 return self._issuer
767
768 @property
769 @_helpers.copy_docstring(google.auth.credentials.Signing)
770 def signer(self):
771 return self._signer