blob: 087dbd9e98afddc2618b04944ccf8e4f9a4f0507 [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 Parrottabcd3ed2016-10-17 11:23:47 -070045import datetime
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070046import json
47
Jon Wayne Parrott54a85172016-10-17 11:27:37 -070048from google.auth import _helpers
Jon Wayne Parrott807032c2016-10-18 09:38:26 -070049from google.auth import _service_account_info
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -070050from google.auth import credentials
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070051from google.auth import crypt
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070052
53
54_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in sections
55_CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
56
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
163 # Make sure the iat and exp claims are present
164 for key in ('iat', 'exp'):
165 if key not in payload:
166 raise ValueError(
167 'Token does not contain required claim {}'.format(key))
168
169 # Make sure the token wasn't issued in the future
170 iat = payload['iat']
171 earliest = iat - _CLOCK_SKEW_SECS
172 if now < earliest:
173 raise ValueError('Token used too early, {} < {}'.format(now, iat))
174
175 # Make sure the token wasn't issue in the past
176 exp = payload['exp']
177 latest = exp + _CLOCK_SKEW_SECS
178 if latest < now:
179 raise ValueError('Token expired, {} < {}'.format(latest, now))
180
181
182def decode(token, certs=None, verify=True, audience=None):
183 """Decode and verify a JWT.
184
185 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700186 token (str): The encoded JWT.
187 certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
188 certificate used to validate the JWT signatyre. If bytes or string,
189 it must the the public key certificate in PEM format. If a mapping,
190 it must be a mapping of key IDs to public key certificates in PEM
191 format. The mapping must contain the same key ID that's specified
192 in the token's header.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700193 verify (bool): Whether to perform signature and claim validation.
194 Verification is done by default.
195 audience (str): The audience claim, 'aud', that this JWT should
196 contain. If None then the JWT's 'aud' parameter is not verified.
197
198 Returns:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700199 Mapping[str, str]: The deserialized JSON payload in the JWT.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700200
201 Raises:
202 ValueError: if any verification checks failed.
203 """
204 header, payload, signed_section, signature = _unverified_decode(token)
205
206 if not verify:
207 return payload
208
209 # If certs is specified as a dictionary of key IDs to certificates, then
210 # use the certificate identified by the key ID in the token header.
211 if isinstance(certs, collections.Mapping):
212 key_id = header.get('kid')
213 if key_id:
214 if key_id not in certs:
215 raise ValueError(
216 'Certificate for key id {} not found.'.format(key_id))
217 certs_to_check = [certs[key_id]]
218 # If there's no key id in the header, check against all of the certs.
219 else:
220 certs_to_check = certs.values()
221 else:
222 certs_to_check = certs
223
224 # Verify that the signature matches the message.
225 if not crypt.verify_signature(signed_section, signature, certs_to_check):
226 raise ValueError('Could not verify token signature.')
227
228 # Verify the issued at and created times in the payload.
229 _verify_iat_and_exp(payload)
230
231 # Check audience.
232 if audience is not None:
233 claim_audience = payload.get('aud')
234 if audience != claim_audience:
235 raise ValueError(
236 'Token has wrong audience {}, expected {}'.format(
237 claim_audience, audience))
238
239 return payload
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700240
241
242class Credentials(credentials.Signing,
243 credentials.Credentials):
244 """Credentials that use a JWT as the bearer token.
245
246 These credentials require an "audience" claim. This claim identifies the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800247 intended recipient of the bearer token.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700248
249 The constructor arguments determine the claims for the JWT that is
250 sent with requests. Usually, you'll construct these credentials with
251 one of the helper constructors as shown in the next section.
252
253 To create JWT credentials using a Google service account private key
254 JSON file::
255
256 credentials = jwt.Credentials.from_service_account_file(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800257 'service-account.json',
258 audience='https://speech.googleapis.com')
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700259
260 If you already have the service account file loaded and parsed::
261
262 service_account_info = json.load(open('service_account.json'))
263 credentials = jwt.Credentials.from_service_account_info(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800264 service_account_info,
265 audience='https://speech.googleapis.com')
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700266
267 Both helper methods pass on arguments to the constructor, so you can
268 specify the JWT claims::
269
270 credentials = jwt.Credentials.from_service_account_file(
271 'service-account.json',
272 audience='https://speech.googleapis.com',
273 additional_claims={'meta': 'data'})
274
275 You can also construct the credentials directly if you have a
276 :class:`~google.auth.crypt.Signer` instance::
277
278 credentials = jwt.Credentials(
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800279 signer,
280 issuer='your-issuer',
281 subject='your-subject',
282 audience=''https://speech.googleapis.com'')
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700283
284 The claims are considered immutable. If you want to modify the claims,
285 you can easily create another instance using :meth:`with_claims`::
286
287 new_credentials = credentials.with_claims(
288 audience='https://vision.googleapis.com')
289 """
290
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800291 def __init__(self, signer, issuer, subject, audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700292 additional_claims=None,
293 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS):
294 """
295 Args:
296 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
297 issuer (str): The `iss` claim.
298 subject (str): The `sub` claim.
299 audience (str): the `aud` claim. The intended audience for the
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800300 credentials.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700301 additional_claims (Mapping[str, str]): Any additional claims for
302 the JWT payload.
303 token_lifetime (int): The amount of time in seconds for
304 which the token is valid. Defaults to 1 hour.
305 """
306 super(Credentials, self).__init__()
307 self._signer = signer
308 self._issuer = issuer
309 self._subject = subject
310 self._audience = audience
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700311 self._token_lifetime = token_lifetime
312
Danny Hermes93d1aa42016-10-17 13:15:07 -0700313 if additional_claims is not None:
314 self._additional_claims = additional_claims
315 else:
316 self._additional_claims = {}
317
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700318 @classmethod
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700319 def _from_signer_and_info(cls, signer, info, **kwargs):
320 """Creates a Credentials instance from a signer and service account
321 info.
322
323 Args:
324 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
325 info (Mapping[str, str]): The service account info.
326 kwargs: Additional arguments to pass to the constructor.
327
328 Returns:
329 google.auth.jwt.Credentials: The constructed credentials.
330
331 Raises:
332 ValueError: If the info is not in the expected format.
333 """
334 kwargs.setdefault('subject', info['client_email'])
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800335 kwargs.setdefault('issuer', info['client_email'])
336 return cls(signer, **kwargs)
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700337
338 @classmethod
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700339 def from_service_account_info(cls, info, **kwargs):
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700340 """Creates a Credentials instance from a dictionary containing service
341 account info in Google format.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700342
343 Args:
344 info (Mapping[str, str]): The service account info in Google
345 format.
346 kwargs: Additional arguments to pass to the constructor.
347
348 Returns:
349 google.auth.jwt.Credentials: The constructed credentials.
350
351 Raises:
352 ValueError: If the info is not in the expected format.
353 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700354 signer = _service_account_info.from_dict(
355 info, require=['client_email'])
356 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700357
358 @classmethod
359 def from_service_account_file(cls, filename, **kwargs):
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700360 """Creates a Credentials instance from a service account .json file
361 in Google format.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700362
363 Args:
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700364 filename (str): The path to the service account .json file.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700365 kwargs: Additional arguments to pass to the constructor.
366
367 Returns:
368 google.auth.jwt.Credentials: The constructed credentials.
369 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700370 info, signer = _service_account_info.from_filename(
371 filename, require=['client_email'])
372 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700373
374 def with_claims(self, issuer=None, subject=None, audience=None,
375 additional_claims=None):
376 """Returns a copy of these credentials with modified claims.
377
378 Args:
379 issuer (str): The `iss` claim. If unspecified the current issuer
380 claim will be used.
381 subject (str): The `sub` claim. If unspecified the current subject
382 claim will be used.
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800383 audience (str): the `aud` claim. If unspecified the current
384 audience claim will be used.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700385 additional_claims (Mapping[str, str]): Any additional claims for
386 the JWT payload. This will be merged with the current
387 additional claims.
388
389 Returns:
390 google.auth.jwt.Credentials: A new credentials instance.
391 """
392 return Credentials(
393 self._signer,
394 issuer=issuer if issuer is not None else self._issuer,
395 subject=subject if subject is not None else self._subject,
396 audience=audience if audience is not None else self._audience,
397 additional_claims=self._additional_claims.copy().update(
398 additional_claims or {}))
399
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800400 def _make_jwt(self):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700401 """Make a signed JWT.
402
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700403 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800404 Tuple[bytes, datetime]: The encoded JWT and the expiration.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700405 """
406 now = _helpers.utcnow()
407 lifetime = datetime.timedelta(seconds=self._token_lifetime)
408 expiry = now + lifetime
409
410 payload = {
411 'iss': self._issuer,
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800412 'sub': self._subject,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700413 'iat': _helpers.datetime_to_secs(now),
414 'exp': _helpers.datetime_to_secs(expiry),
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800415 'aud': self._audience,
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700416 }
417
418 payload.update(self._additional_claims)
419
420 jwt = encode(self._signer, payload)
421
422 return jwt, expiry
423
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700424 def refresh(self, request):
425 """Refreshes the access token.
426
427 Args:
428 request (Any): Unused.
429 """
430 # pylint: disable=unused-argument
431 # (pylint doesn't correctly recognize overridden methods.)
432 self.token, self.expiry = self._make_jwt()
433
Jon Wayne Parrottab086892017-02-23 09:20:14 -0800434 @_helpers.copy_docstring(credentials.Signing)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700435 def sign_bytes(self, message):
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700436 return self._signer.sign(message)
437
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800438 @property
439 @_helpers.copy_docstring(credentials.Signing)
440 def signer_email(self):
441 return self._issuer
442
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800443 @property
444 @_helpers.copy_docstring(credentials.Signing)
445 def signer(self):
446 return self._signer