blob: 506ba0ec850141dc7555764ded35ae97fdd31d7d [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
55_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in sections
56_CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
57
58
59def encode(signer, payload, header=None, key_id=None):
60 """Make a signed JWT.
61
62 Args:
63 signer (google.auth.crypt.Signer): The signer used to sign the JWT.
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -070064 payload (Mapping[str, str]): The JWT payload.
65 header (Mapping[str, str]): Additional JWT header payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070066 key_id (str): The key id to add to the JWT header. If the
67 signer has a key id it will be used as the default. If this is
68 specified it will override the signer's key id.
69
70 Returns:
71 bytes: The encoded JWT.
72 """
73 if header is None:
74 header = {}
75
76 if key_id is None:
77 key_id = signer.key_id
78
79 header.update({'typ': 'JWT', 'alg': 'RS256'})
80
81 if key_id is not None:
82 header['kid'] = key_id
83
84 segments = [
85 base64.urlsafe_b64encode(json.dumps(header).encode('utf-8')),
86 base64.urlsafe_b64encode(json.dumps(payload).encode('utf-8')),
87 ]
88
89 signing_input = b'.'.join(segments)
90 signature = signer.sign(signing_input)
91 segments.append(base64.urlsafe_b64encode(signature))
92
93 return b'.'.join(segments)
94
95
96def _decode_jwt_segment(encoded_section):
97 """Decodes a single JWT segment."""
Jon Wayne Parrott97eb8702016-11-17 09:43:16 -080098 section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070099 try:
100 return json.loads(section_bytes.decode('utf-8'))
101 except ValueError:
102 raise ValueError('Can\'t parse segment: {0}'.format(section_bytes))
103
104
105def _unverified_decode(token):
106 """Decodes a token and does no verification.
107
108 Args:
109 token (Union[str, bytes]): The encoded JWT.
110
111 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800112 Tuple[str, str, str, str]: header, payload, signed_section, and
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700113 signature.
114
115 Raises:
116 ValueError: if there are an incorrect amount of segments in the token.
117 """
118 token = _helpers.to_bytes(token)
119
120 if token.count(b'.') != 2:
121 raise ValueError(
122 'Wrong number of segments in token: {0}'.format(token))
123
124 encoded_header, encoded_payload, signature = token.split(b'.')
125 signed_section = encoded_header + b'.' + encoded_payload
Jon Wayne Parrott97eb8702016-11-17 09:43:16 -0800126 signature = _helpers.padded_urlsafe_b64decode(signature)
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700127
128 # Parse segments
129 header = _decode_jwt_segment(encoded_header)
130 payload = _decode_jwt_segment(encoded_payload)
131
132 return header, payload, signed_section, signature
133
134
135def decode_header(token):
136 """Return the decoded header of a token.
137
138 No verification is done. This is useful to extract the key id from
139 the header in order to acquire the appropriate certificate to verify
140 the token.
141
142 Args:
143 token (Union[str, bytes]): the encoded JWT.
144
145 Returns:
146 Mapping: The decoded JWT header.
147 """
148 header, _, _, _ = _unverified_decode(token)
149 return header
150
151
152def _verify_iat_and_exp(payload):
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700153 """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700154 payload.
155
156 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700157 payload (Mapping[str, str]): The JWT payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700158
159 Raises:
160 ValueError: if any checks failed.
161 """
162 now = _helpers.datetime_to_secs(_helpers.utcnow())
163
164 # Make sure the iat and exp claims are present
165 for key in ('iat', 'exp'):
166 if key not in payload:
167 raise ValueError(
168 'Token does not contain required claim {}'.format(key))
169
170 # Make sure the token wasn't issued in the future
171 iat = payload['iat']
172 earliest = iat - _CLOCK_SKEW_SECS
173 if now < earliest:
174 raise ValueError('Token used too early, {} < {}'.format(now, iat))
175
176 # Make sure the token wasn't issue in the past
177 exp = payload['exp']
178 latest = exp + _CLOCK_SKEW_SECS
179 if latest < now:
180 raise ValueError('Token expired, {} < {}'.format(latest, now))
181
182
183def decode(token, certs=None, verify=True, audience=None):
184 """Decode and verify a JWT.
185
186 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700187 token (str): The encoded JWT.
188 certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
189 certificate used to validate the JWT signatyre. If bytes or string,
190 it must the the public key certificate in PEM format. If a mapping,
191 it must be a mapping of key IDs to public key certificates in PEM
192 format. The mapping must contain the same key ID that's specified
193 in the token's header.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700194 verify (bool): Whether to perform signature and claim validation.
195 Verification is done by default.
196 audience (str): The audience claim, 'aud', that this JWT should
197 contain. If None then the JWT's 'aud' parameter is not verified.
198
199 Returns:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700200 Mapping[str, str]: The deserialized JSON payload in the JWT.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700201
202 Raises:
203 ValueError: if any verification checks failed.
204 """
205 header, payload, signed_section, signature = _unverified_decode(token)
206
207 if not verify:
208 return payload
209
210 # If certs is specified as a dictionary of key IDs to certificates, then
211 # use the certificate identified by the key ID in the token header.
212 if isinstance(certs, collections.Mapping):
213 key_id = header.get('kid')
214 if key_id:
215 if key_id not in certs:
216 raise ValueError(
217 'Certificate for key id {} not found.'.format(key_id))
218 certs_to_check = [certs[key_id]]
219 # If there's no key id in the header, check against all of the certs.
220 else:
221 certs_to_check = certs.values()
222 else:
223 certs_to_check = certs
224
225 # Verify that the signature matches the message.
226 if not crypt.verify_signature(signed_section, signature, certs_to_check):
227 raise ValueError('Could not verify token signature.')
228
229 # Verify the issued at and created times in the payload.
230 _verify_iat_and_exp(payload)
231
232 # Check audience.
233 if audience is not None:
234 claim_audience = payload.get('aud')
235 if audience != claim_audience:
236 raise ValueError(
237 'Token has wrong audience {}, expected {}'.format(
238 claim_audience, audience))
239
240 return payload
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700241
242
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800243class Credentials(google.auth.credentials.Signing,
244 google.auth.credentials.Credentials):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700245 """Credentials that use a JWT as the bearer token.
246
247 These credentials require an "audience" claim. This claim identifies the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800248 intended recipient of the bearer token.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700249
250 The constructor arguments determine the claims for the JWT that is
251 sent with requests. Usually, you'll construct these credentials with
252 one of the helper constructors as shown in the next section.
253
254 To create JWT credentials using a Google service account private key
255 JSON file::
256
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800257 audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700258 credentials = jwt.Credentials.from_service_account_file(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800259 'service-account.json',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800260 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700261
262 If you already have the service account file loaded and parsed::
263
264 service_account_info = json.load(open('service_account.json'))
265 credentials = jwt.Credentials.from_service_account_info(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800266 service_account_info,
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800267 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700268
269 Both helper methods pass on arguments to the constructor, so you can
270 specify the JWT claims::
271
272 credentials = jwt.Credentials.from_service_account_file(
273 'service-account.json',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800274 audience=audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700275 additional_claims={'meta': 'data'})
276
277 You can also construct the credentials directly if you have a
278 :class:`~google.auth.crypt.Signer` instance::
279
280 credentials = jwt.Credentials(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800281 signer,
282 issuer='your-issuer',
283 subject='your-subject',
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800284 audience=audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700285
286 The claims are considered immutable. If you want to modify the claims,
287 you can easily create another instance using :meth:`with_claims`::
288
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800289 new_audience = (
290 'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
291 new_credentials = credentials.with_claims(audience=new_audience)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700292 """
293
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800294 def __init__(self, signer, issuer, subject, audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700295 additional_claims=None,
296 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS):
297 """
298 Args:
299 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
300 issuer (str): The `iss` claim.
301 subject (str): The `sub` claim.
302 audience (str): the `aud` claim. The intended audience for the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800303 credentials.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700304 additional_claims (Mapping[str, str]): Any additional claims for
305 the JWT payload.
306 token_lifetime (int): The amount of time in seconds for
307 which the token is valid. Defaults to 1 hour.
308 """
309 super(Credentials, self).__init__()
310 self._signer = signer
311 self._issuer = issuer
312 self._subject = subject
313 self._audience = audience
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700314 self._token_lifetime = token_lifetime
315
Danny Hermes93d1aa42016-10-17 13:15:07 -0700316 if additional_claims is not None:
317 self._additional_claims = additional_claims
318 else:
319 self._additional_claims = {}
320
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700321 @classmethod
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700322 def _from_signer_and_info(cls, signer, info, **kwargs):
323 """Creates a Credentials instance from a signer and service account
324 info.
325
326 Args:
327 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
328 info (Mapping[str, str]): The service account info.
329 kwargs: Additional arguments to pass to the constructor.
330
331 Returns:
332 google.auth.jwt.Credentials: The constructed credentials.
333
334 Raises:
335 ValueError: If the info is not in the expected format.
336 """
337 kwargs.setdefault('subject', info['client_email'])
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800338 kwargs.setdefault('issuer', info['client_email'])
339 return cls(signer, **kwargs)
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700340
341 @classmethod
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700342 def from_service_account_info(cls, info, **kwargs):
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700343 """Creates a Credentials instance from a dictionary containing service
344 account info in Google format.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700345
346 Args:
347 info (Mapping[str, str]): The service account info in Google
348 format.
349 kwargs: Additional arguments to pass to the constructor.
350
351 Returns:
352 google.auth.jwt.Credentials: The constructed credentials.
353
354 Raises:
355 ValueError: If the info is not in the expected format.
356 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700357 signer = _service_account_info.from_dict(
358 info, require=['client_email'])
359 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700360
361 @classmethod
362 def from_service_account_file(cls, filename, **kwargs):
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700363 """Creates a Credentials instance from a service account .json file
364 in Google format.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700365
366 Args:
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700367 filename (str): The path to the service account .json file.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700368 kwargs: Additional arguments to pass to the constructor.
369
370 Returns:
371 google.auth.jwt.Credentials: The constructed credentials.
372 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700373 info, signer = _service_account_info.from_filename(
374 filename, require=['client_email'])
375 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700376
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800377 @classmethod
378 def from_signing_credentials(cls, credentials, audience, **kwargs):
379 """Creates a new :class:`google.auth.jwt.Credentials` instance from an
380 existing :class:`google.auth.credentials.Signing` instance.
381
382 The new instance will use the same signer as the existing instance and
383 will use the existing instance's signer email as the issuer and
384 subject by default.
385
386 Example::
387
388 svc_creds = service_account.Credentials.from_service_account_file(
389 'service_account.json')
390 audience = (
391 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher')
392 jwt_creds = jwt.Credentials.from_signing_credentials(
393 svc_creds, audience=audience)
394
395 Args:
396 credentials (google.auth.credentials.Signing): The credentials to
397 use to construct the new credentials.
398 audience (str): the `aud` claim. The intended audience for the
399 credentials.
400 kwargs: Additional arguments to pass to the constructor.
401
402 Returns:
403 google.auth.jwt.Credentials: A new Credentials instance.
404 """
405 kwargs.setdefault('issuer', credentials.signer_email)
406 kwargs.setdefault('subject', credentials.signer_email)
407 return cls(
408 credentials.signer,
409 audience=audience,
410 **kwargs)
411
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700412 def with_claims(self, issuer=None, subject=None, audience=None,
413 additional_claims=None):
414 """Returns a copy of these credentials with modified claims.
415
416 Args:
417 issuer (str): The `iss` claim. If unspecified the current issuer
418 claim will be used.
419 subject (str): The `sub` claim. If unspecified the current subject
420 claim will be used.
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800421 audience (str): the `aud` claim. If unspecified the current
422 audience claim will be used.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700423 additional_claims (Mapping[str, str]): Any additional claims for
424 the JWT payload. This will be merged with the current
425 additional claims.
426
427 Returns:
428 google.auth.jwt.Credentials: A new credentials instance.
429 """
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700430 new_additional_claims = copy.deepcopy(self._additional_claims)
431 new_additional_claims.update(additional_claims or {})
432
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700433 return Credentials(
434 self._signer,
435 issuer=issuer if issuer is not None else self._issuer,
436 subject=subject if subject is not None else self._subject,
437 audience=audience if audience is not None else self._audience,
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700438 additional_claims=new_additional_claims)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700439
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800440 def _make_jwt(self):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700441 """Make a signed JWT.
442
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700443 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800444 Tuple[bytes, datetime]: The encoded JWT and the expiration.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700445 """
446 now = _helpers.utcnow()
447 lifetime = datetime.timedelta(seconds=self._token_lifetime)
448 expiry = now + lifetime
449
450 payload = {
451 'iss': self._issuer,
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800452 'sub': self._subject,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700453 'iat': _helpers.datetime_to_secs(now),
454 'exp': _helpers.datetime_to_secs(expiry),
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800455 'aud': self._audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700456 }
457
458 payload.update(self._additional_claims)
459
460 jwt = encode(self._signer, payload)
461
462 return jwt, expiry
463
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700464 def refresh(self, request):
465 """Refreshes the access token.
466
467 Args:
468 request (Any): Unused.
469 """
470 # pylint: disable=unused-argument
471 # (pylint doesn't correctly recognize overridden methods.)
472 self.token, self.expiry = self._make_jwt()
473
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800474 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700475 def sign_bytes(self, message):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700476 return self._signer.sign(message)
477
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800478 @property
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800479 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800480 def signer_email(self):
481 return self._issuer
482
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800483 @property
Jon Wayne Parrottb8f48d02017-02-24 09:03:24 -0800484 @_helpers.copy_docstring(google.auth.credentials.Signing)
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800485 def signer(self):
486 return self._signer