blob: 412f122e347f1defc57d0e9c5d03822ecae86112 [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
24 from google.auth import crypto
25 from google.auth import jwt
26
27 signer = crypt.Signer(private_key)
28 payload = {'some': 'payload'}
29 encoded = jwt.encode(signer, payload)
30
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
43import base64
44import collections
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -070045import copy
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -070046import datetime
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070047import json
48
Jon Wayne Parrott54a85172016-10-17 11:27:37 -070049from google.auth import _helpers
Jon Wayne Parrott807032c2016-10-18 09:38:26 -070050from google.auth import _service_account_info
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070051from google.auth import crypt
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -080052import google.auth.credentials
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070053
54
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -070055_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070056
57
58def encode(signer, payload, header=None, key_id=None):
59 """Make a signed JWT.
60
61 Args:
62 signer (google.auth.crypt.Signer): The signer used to sign the JWT.
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -070063 payload (Mapping[str, str]): The JWT payload.
64 header (Mapping[str, str]): Additional JWT header payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070065 key_id (str): The key id to add to the JWT header. If the
66 signer has a key id it will be used as the default. If this is
67 specified it will override the signer's key id.
68
69 Returns:
70 bytes: The encoded JWT.
71 """
72 if header is None:
73 header = {}
74
75 if key_id is None:
76 key_id = signer.key_id
77
78 header.update({'typ': 'JWT', 'alg': 'RS256'})
79
80 if key_id is not None:
81 header['kid'] = key_id
82
83 segments = [
84 base64.urlsafe_b64encode(json.dumps(header).encode('utf-8')),
85 base64.urlsafe_b64encode(json.dumps(payload).encode('utf-8')),
86 ]
87
88 signing_input = b'.'.join(segments)
89 signature = signer.sign(signing_input)
90 segments.append(base64.urlsafe_b64encode(signature))
91
92 return b'.'.join(segments)
93
94
95def _decode_jwt_segment(encoded_section):
96 """Decodes a single JWT segment."""
Jon Wayne Parrott97eb8702016-11-17 09:43:16 -080097 section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070098 try:
99 return json.loads(section_bytes.decode('utf-8'))
100 except ValueError:
101 raise ValueError('Can\'t parse segment: {0}'.format(section_bytes))
102
103
104def _unverified_decode(token):
105 """Decodes a token and does no verification.
106
107 Args:
108 token (Union[str, bytes]): The encoded JWT.
109
110 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800111 Tuple[str, str, str, str]: header, payload, signed_section, and
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700112 signature.
113
114 Raises:
115 ValueError: if there are an incorrect amount of segments in the token.
116 """
117 token = _helpers.to_bytes(token)
118
119 if token.count(b'.') != 2:
120 raise ValueError(
121 'Wrong number of segments in token: {0}'.format(token))
122
123 encoded_header, encoded_payload, signature = token.split(b'.')
124 signed_section = encoded_header + b'.' + encoded_payload
Jon Wayne Parrott97eb8702016-11-17 09:43:16 -0800125 signature = _helpers.padded_urlsafe_b64decode(signature)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700126
127 # Parse segments
128 header = _decode_jwt_segment(encoded_header)
129 payload = _decode_jwt_segment(encoded_payload)
130
131 return header, payload, signed_section, signature
132
133
134def decode_header(token):
135 """Return the decoded header of a token.
136
137 No verification is done. This is useful to extract the key id from
138 the header in order to acquire the appropriate certificate to verify
139 the token.
140
141 Args:
142 token (Union[str, bytes]): the encoded JWT.
143
144 Returns:
145 Mapping: The decoded JWT header.
146 """
147 header, _, _, _ = _unverified_decode(token)
148 return header
149
150
151def _verify_iat_and_exp(payload):
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700152 """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700153 payload.
154
155 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700156 payload (Mapping[str, str]): The JWT payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700157
158 Raises:
159 ValueError: if any checks failed.
160 """
161 now = _helpers.datetime_to_secs(_helpers.utcnow())
162
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700163 # Make sure the iat and exp claims are present.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700164 for key in ('iat', 'exp'):
165 if key not in payload:
166 raise ValueError(
167 'Token does not contain required claim {}'.format(key))
168
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700169 # Make sure the token wasn't issued in the future.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700170 iat = payload['iat']
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700171 # Err on the side of accepting a token that is slightly early to account
172 # for clock skew.
173 earliest = iat - _helpers.CLOCK_SKEW_SECS
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700174 if now < earliest:
175 raise ValueError('Token used too early, {} < {}'.format(now, iat))
176
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700177 # Make sure the token wasn't issued in the past.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700178 exp = payload['exp']
Jon Wayne Parrotte60c1242017-03-23 16:00:24 -0700179 # Err on the side of accepting a token that is slightly out of date
180 # to account for clow skew.
181 latest = exp + _helpers.CLOCK_SKEW_SECS
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700182 if latest < now:
183 raise ValueError('Token expired, {} < {}'.format(latest, now))
184
185
186def decode(token, certs=None, verify=True, audience=None):
187 """Decode and verify a JWT.
188
189 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700190 token (str): The encoded JWT.
191 certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
192 certificate used to validate the JWT signatyre. If bytes or string,
193 it must the the public key certificate in PEM format. If a mapping,
194 it must be a mapping of key IDs to public key certificates in PEM
195 format. The mapping must contain the same key ID that's specified
196 in the token's header.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700197 verify (bool): Whether to perform signature and claim validation.
198 Verification is done by default.
199 audience (str): The audience claim, 'aud', that this JWT should
200 contain. If None then the JWT's 'aud' parameter is not verified.
201
202 Returns:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700203 Mapping[str, str]: The deserialized JSON payload in the JWT.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700204
205 Raises:
206 ValueError: if any verification checks failed.
207 """
208 header, payload, signed_section, signature = _unverified_decode(token)
209
210 if not verify:
211 return payload
212
213 # If certs is specified as a dictionary of key IDs to certificates, then
214 # use the certificate identified by the key ID in the token header.
215 if isinstance(certs, collections.Mapping):
216 key_id = header.get('kid')
217 if key_id:
218 if key_id not in certs:
219 raise ValueError(
220 'Certificate for key id {} not found.'.format(key_id))
221 certs_to_check = [certs[key_id]]
222 # If there's no key id in the header, check against all of the certs.
223 else:
224 certs_to_check = certs.values()
225 else:
226 certs_to_check = certs
227
228 # Verify that the signature matches the message.
229 if not crypt.verify_signature(signed_section, signature, certs_to_check):
230 raise ValueError('Could not verify token signature.')
231
232 # Verify the issued at and created times in the payload.
233 _verify_iat_and_exp(payload)
234
235 # Check audience.
236 if audience is not None:
237 claim_audience = payload.get('aud')
238 if audience != claim_audience:
239 raise ValueError(
240 'Token has wrong audience {}, expected {}'.format(
241 claim_audience, audience))
242
243 return payload
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700244
245
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800246class Credentials(google.auth.credentials.Signing,
247 google.auth.credentials.Credentials):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700248 """Credentials that use a JWT as the bearer token.
249
250 These credentials require an "audience" claim. This claim identifies the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800251 intended recipient of the bearer token.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700252
253 The constructor arguments determine the claims for the JWT that is
254 sent with requests. Usually, you'll construct these credentials with
255 one of the helper constructors as shown in the next section.
256
257 To create JWT credentials using a Google service account private key
258 JSON file::
259
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800260 audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700261 credentials = jwt.Credentials.from_service_account_file(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800262 'service-account.json',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800263 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700264
265 If you already have the service account file loaded and parsed::
266
267 service_account_info = json.load(open('service_account.json'))
268 credentials = jwt.Credentials.from_service_account_info(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800269 service_account_info,
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800270 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700271
272 Both helper methods pass on arguments to the constructor, so you can
273 specify the JWT claims::
274
275 credentials = jwt.Credentials.from_service_account_file(
276 'service-account.json',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800277 audience=audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700278 additional_claims={'meta': 'data'})
279
280 You can also construct the credentials directly if you have a
281 :class:`~google.auth.crypt.Signer` instance::
282
283 credentials = jwt.Credentials(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800284 signer,
285 issuer='your-issuer',
286 subject='your-subject',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800287 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700288
289 The claims are considered immutable. If you want to modify the claims,
290 you can easily create another instance using :meth:`with_claims`::
291
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800292 new_audience = (
293 'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
294 new_credentials = credentials.with_claims(audience=new_audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700295 """
296
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800297 def __init__(self, signer, issuer, subject, audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700298 additional_claims=None,
299 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS):
300 """
301 Args:
302 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
303 issuer (str): The `iss` claim.
304 subject (str): The `sub` claim.
305 audience (str): the `aud` claim. The intended audience for the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800306 credentials.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700307 additional_claims (Mapping[str, str]): Any additional claims for
308 the JWT payload.
309 token_lifetime (int): The amount of time in seconds for
310 which the token is valid. Defaults to 1 hour.
311 """
312 super(Credentials, self).__init__()
313 self._signer = signer
314 self._issuer = issuer
315 self._subject = subject
316 self._audience = audience
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700317 self._token_lifetime = token_lifetime
318
Danny Hermes93d1aa42016-10-17 13:15:07 -0700319 if additional_claims is not None:
320 self._additional_claims = additional_claims
321 else:
322 self._additional_claims = {}
323
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700324 @classmethod
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700325 def _from_signer_and_info(cls, signer, info, **kwargs):
326 """Creates a Credentials instance from a signer and service account
327 info.
328
329 Args:
330 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
331 info (Mapping[str, str]): The service account info.
332 kwargs: Additional arguments to pass to the constructor.
333
334 Returns:
335 google.auth.jwt.Credentials: The constructed credentials.
336
337 Raises:
338 ValueError: If the info is not in the expected format.
339 """
340 kwargs.setdefault('subject', info['client_email'])
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800341 kwargs.setdefault('issuer', info['client_email'])
342 return cls(signer, **kwargs)
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700343
344 @classmethod
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700345 def from_service_account_info(cls, info, **kwargs):
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700346 """Creates a Credentials instance from a dictionary containing service
347 account info in Google format.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700348
349 Args:
350 info (Mapping[str, str]): The service account info in Google
351 format.
352 kwargs: Additional arguments to pass to the constructor.
353
354 Returns:
355 google.auth.jwt.Credentials: The constructed credentials.
356
357 Raises:
358 ValueError: If the info is not in the expected format.
359 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700360 signer = _service_account_info.from_dict(
361 info, require=['client_email'])
362 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700363
364 @classmethod
365 def from_service_account_file(cls, filename, **kwargs):
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700366 """Creates a Credentials instance from a service account .json file
367 in Google format.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700368
369 Args:
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700370 filename (str): The path to the service account .json file.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700371 kwargs: Additional arguments to pass to the constructor.
372
373 Returns:
374 google.auth.jwt.Credentials: The constructed credentials.
375 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700376 info, signer = _service_account_info.from_filename(
377 filename, require=['client_email'])
378 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700379
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800380 @classmethod
381 def from_signing_credentials(cls, credentials, audience, **kwargs):
382 """Creates a new :class:`google.auth.jwt.Credentials` instance from an
383 existing :class:`google.auth.credentials.Signing` instance.
384
385 The new instance will use the same signer as the existing instance and
386 will use the existing instance's signer email as the issuer and
387 subject by default.
388
389 Example::
390
391 svc_creds = service_account.Credentials.from_service_account_file(
392 'service_account.json')
393 audience = (
394 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher')
395 jwt_creds = jwt.Credentials.from_signing_credentials(
396 svc_creds, audience=audience)
397
398 Args:
399 credentials (google.auth.credentials.Signing): The credentials to
400 use to construct the new credentials.
401 audience (str): the `aud` claim. The intended audience for the
402 credentials.
403 kwargs: Additional arguments to pass to the constructor.
404
405 Returns:
406 google.auth.jwt.Credentials: A new Credentials instance.
407 """
408 kwargs.setdefault('issuer', credentials.signer_email)
409 kwargs.setdefault('subject', credentials.signer_email)
410 return cls(
411 credentials.signer,
412 audience=audience,
413 **kwargs)
414
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700415 def with_claims(self, issuer=None, subject=None, audience=None,
416 additional_claims=None):
417 """Returns a copy of these credentials with modified claims.
418
419 Args:
420 issuer (str): The `iss` claim. If unspecified the current issuer
421 claim will be used.
422 subject (str): The `sub` claim. If unspecified the current subject
423 claim will be used.
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800424 audience (str): the `aud` claim. If unspecified the current
425 audience claim will be used.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700426 additional_claims (Mapping[str, str]): Any additional claims for
427 the JWT payload. This will be merged with the current
428 additional claims.
429
430 Returns:
431 google.auth.jwt.Credentials: A new credentials instance.
432 """
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700433 new_additional_claims = copy.deepcopy(self._additional_claims)
434 new_additional_claims.update(additional_claims or {})
435
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700436 return Credentials(
437 self._signer,
438 issuer=issuer if issuer is not None else self._issuer,
439 subject=subject if subject is not None else self._subject,
440 audience=audience if audience is not None else self._audience,
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700441 additional_claims=new_additional_claims)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700442
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800443 def _make_jwt(self):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700444 """Make a signed JWT.
445
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700446 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800447 Tuple[bytes, datetime]: The encoded JWT and the expiration.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700448 """
449 now = _helpers.utcnow()
450 lifetime = datetime.timedelta(seconds=self._token_lifetime)
451 expiry = now + lifetime
452
453 payload = {
454 'iss': self._issuer,
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800455 'sub': self._subject,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700456 'iat': _helpers.datetime_to_secs(now),
457 'exp': _helpers.datetime_to_secs(expiry),
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800458 'aud': self._audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700459 }
460
461 payload.update(self._additional_claims)
462
463 jwt = encode(self._signer, payload)
464
465 return jwt, expiry
466
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700467 def refresh(self, request):
468 """Refreshes the access token.
469
470 Args:
471 request (Any): Unused.
472 """
473 # pylint: disable=unused-argument
474 # (pylint doesn't correctly recognize overridden methods.)
475 self.token, self.expiry = self._make_jwt()
476
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800477 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700478 def sign_bytes(self, message):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700479 return self._signer.sign(message)
480
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800481 @property
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800482 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800483 def signer_email(self):
484 return self._issuer
485
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800486 @property
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800487 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800488 def signer(self):
489 return self._signer