blob: 7abe4c2eea70923e00c56a61714c341af38f3438 [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 Parrottabcd3ed2016-10-17 11:23:47 -070048from six.moves import urllib
49
Jon Wayne Parrott54a85172016-10-17 11:27:37 -070050from google.auth import _helpers
Jon Wayne Parrott807032c2016-10-18 09:38:26 -070051from google.auth import _service_account_info
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -070052from google.auth import credentials
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070053from google.auth import crypt
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070054
55
56_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in sections
57_CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
58
59
60def encode(signer, payload, header=None, key_id=None):
61 """Make a signed JWT.
62
63 Args:
64 signer (google.auth.crypt.Signer): The signer used to sign the JWT.
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -070065 payload (Mapping[str, str]): The JWT payload.
66 header (Mapping[str, str]): Additional JWT header payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070067 key_id (str): The key id to add to the JWT header. If the
68 signer has a key id it will be used as the default. If this is
69 specified it will override the signer's key id.
70
71 Returns:
72 bytes: The encoded JWT.
73 """
74 if header is None:
75 header = {}
76
77 if key_id is None:
78 key_id = signer.key_id
79
80 header.update({'typ': 'JWT', 'alg': 'RS256'})
81
82 if key_id is not None:
83 header['kid'] = key_id
84
85 segments = [
86 base64.urlsafe_b64encode(json.dumps(header).encode('utf-8')),
87 base64.urlsafe_b64encode(json.dumps(payload).encode('utf-8')),
88 ]
89
90 signing_input = b'.'.join(segments)
91 signature = signer.sign(signing_input)
92 segments.append(base64.urlsafe_b64encode(signature))
93
94 return b'.'.join(segments)
95
96
97def _decode_jwt_segment(encoded_section):
98 """Decodes a single JWT segment."""
99 section_bytes = base64.urlsafe_b64decode(encoded_section)
100 try:
101 return json.loads(section_bytes.decode('utf-8'))
102 except ValueError:
103 raise ValueError('Can\'t parse segment: {0}'.format(section_bytes))
104
105
106def _unverified_decode(token):
107 """Decodes a token and does no verification.
108
109 Args:
110 token (Union[str, bytes]): The encoded JWT.
111
112 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800113 Tuple[str, str, str, str]: header, payload, signed_section, and
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700114 signature.
115
116 Raises:
117 ValueError: if there are an incorrect amount of segments in the token.
118 """
119 token = _helpers.to_bytes(token)
120
121 if token.count(b'.') != 2:
122 raise ValueError(
123 'Wrong number of segments in token: {0}'.format(token))
124
125 encoded_header, encoded_payload, signature = token.split(b'.')
126 signed_section = encoded_header + b'.' + encoded_payload
127 signature = base64.urlsafe_b64decode(signature)
128
129 # Parse segments
130 header = _decode_jwt_segment(encoded_header)
131 payload = _decode_jwt_segment(encoded_payload)
132
133 return header, payload, signed_section, signature
134
135
136def decode_header(token):
137 """Return the decoded header of a token.
138
139 No verification is done. This is useful to extract the key id from
140 the header in order to acquire the appropriate certificate to verify
141 the token.
142
143 Args:
144 token (Union[str, bytes]): the encoded JWT.
145
146 Returns:
147 Mapping: The decoded JWT header.
148 """
149 header, _, _, _ = _unverified_decode(token)
150 return header
151
152
153def _verify_iat_and_exp(payload):
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700154 """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700155 payload.
156
157 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700158 payload (Mapping[str, str]): The JWT payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700159
160 Raises:
161 ValueError: if any checks failed.
162 """
163 now = _helpers.datetime_to_secs(_helpers.utcnow())
164
165 # Make sure the iat and exp claims are present
166 for key in ('iat', 'exp'):
167 if key not in payload:
168 raise ValueError(
169 'Token does not contain required claim {}'.format(key))
170
171 # Make sure the token wasn't issued in the future
172 iat = payload['iat']
173 earliest = iat - _CLOCK_SKEW_SECS
174 if now < earliest:
175 raise ValueError('Token used too early, {} < {}'.format(now, iat))
176
177 # Make sure the token wasn't issue in the past
178 exp = payload['exp']
179 latest = exp + _CLOCK_SKEW_SECS
180 if latest < now:
181 raise ValueError('Token expired, {} < {}'.format(latest, now))
182
183
184def decode(token, certs=None, verify=True, audience=None):
185 """Decode and verify a JWT.
186
187 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700188 token (str): The encoded JWT.
189 certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
190 certificate used to validate the JWT signatyre. If bytes or string,
191 it must the the public key certificate in PEM format. If a mapping,
192 it must be a mapping of key IDs to public key certificates in PEM
193 format. The mapping must contain the same key ID that's specified
194 in the token's header.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700195 verify (bool): Whether to perform signature and claim validation.
196 Verification is done by default.
197 audience (str): The audience claim, 'aud', that this JWT should
198 contain. If None then the JWT's 'aud' parameter is not verified.
199
200 Returns:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700201 Mapping[str, str]: The deserialized JSON payload in the JWT.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700202
203 Raises:
204 ValueError: if any verification checks failed.
205 """
206 header, payload, signed_section, signature = _unverified_decode(token)
207
208 if not verify:
209 return payload
210
211 # If certs is specified as a dictionary of key IDs to certificates, then
212 # use the certificate identified by the key ID in the token header.
213 if isinstance(certs, collections.Mapping):
214 key_id = header.get('kid')
215 if key_id:
216 if key_id not in certs:
217 raise ValueError(
218 'Certificate for key id {} not found.'.format(key_id))
219 certs_to_check = [certs[key_id]]
220 # If there's no key id in the header, check against all of the certs.
221 else:
222 certs_to_check = certs.values()
223 else:
224 certs_to_check = certs
225
226 # Verify that the signature matches the message.
227 if not crypt.verify_signature(signed_section, signature, certs_to_check):
228 raise ValueError('Could not verify token signature.')
229
230 # Verify the issued at and created times in the payload.
231 _verify_iat_and_exp(payload)
232
233 # Check audience.
234 if audience is not None:
235 claim_audience = payload.get('aud')
236 if audience != claim_audience:
237 raise ValueError(
238 'Token has wrong audience {}, expected {}'.format(
239 claim_audience, audience))
240
241 return payload
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700242
243
244class Credentials(credentials.Signing,
245 credentials.Credentials):
246 """Credentials that use a JWT as the bearer token.
247
248 These credentials require an "audience" claim. This claim identifies the
249 intended recipient of the bearer token. You can set the audience when
250 you construct these credentials, however, these credentials can also set
251 the audience claim automatically if not specified. In this case, whenever
252 a request is made the credentials will automatically generate a one-time
253 JWT with the request URI as the audience.
254
255 The constructor arguments determine the claims for the JWT that is
256 sent with requests. Usually, you'll construct these credentials with
257 one of the helper constructors as shown in the next section.
258
259 To create JWT credentials using a Google service account private key
260 JSON file::
261
262 credentials = jwt.Credentials.from_service_account_file(
263 'service-account.json')
264
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(
269 service_account_info)
270
271 Both helper methods pass on arguments to the constructor, so you can
272 specify the JWT claims::
273
274 credentials = jwt.Credentials.from_service_account_file(
275 'service-account.json',
276 audience='https://speech.googleapis.com',
277 additional_claims={'meta': 'data'})
278
279 You can also construct the credentials directly if you have a
280 :class:`~google.auth.crypt.Signer` instance::
281
282 credentials = jwt.Credentials(
283 signer, issuer='your-issuer', subject='your-subject')
284
285 The claims are considered immutable. If you want to modify the claims,
286 you can easily create another instance using :meth:`with_claims`::
287
288 new_credentials = credentials.with_claims(
289 audience='https://vision.googleapis.com')
290 """
291
292 def __init__(self, signer, issuer=None, subject=None, audience=None,
293 additional_claims=None,
294 token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS):
295 """
296 Args:
297 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
298 issuer (str): The `iss` claim.
299 subject (str): The `sub` claim.
300 audience (str): the `aud` claim. The intended audience for the
301 credentials. If not specified, a new JWT will be generated for
302 every request and will use the request URI as the audience.
303 additional_claims (Mapping[str, str]): Any additional claims for
304 the JWT payload.
305 token_lifetime (int): The amount of time in seconds for
306 which the token is valid. Defaults to 1 hour.
307 """
308 super(Credentials, self).__init__()
309 self._signer = signer
310 self._issuer = issuer
311 self._subject = subject
312 self._audience = audience
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700313 self._token_lifetime = token_lifetime
314
Danny Hermes93d1aa42016-10-17 13:15:07 -0700315 if additional_claims is not None:
316 self._additional_claims = additional_claims
317 else:
318 self._additional_claims = {}
319
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700320 @classmethod
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700321 def _from_signer_and_info(cls, signer, info, **kwargs):
322 """Creates a Credentials instance from a signer and service account
323 info.
324
325 Args:
326 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
327 info (Mapping[str, str]): The service account info.
328 kwargs: Additional arguments to pass to the constructor.
329
330 Returns:
331 google.auth.jwt.Credentials: The constructed credentials.
332
333 Raises:
334 ValueError: If the info is not in the expected format.
335 """
336 kwargs.setdefault('subject', info['client_email'])
337 return cls(signer, issuer=info['client_email'], **kwargs)
338
339 @classmethod
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700340 def from_service_account_info(cls, info, **kwargs):
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700341 """Creates a Credentials instance from a dictionary containing service
342 account info in Google format.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700343
344 Args:
345 info (Mapping[str, str]): The service account info in Google
346 format.
347 kwargs: Additional arguments to pass to the constructor.
348
349 Returns:
350 google.auth.jwt.Credentials: The constructed credentials.
351
352 Raises:
353 ValueError: If the info is not in the expected format.
354 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700355 signer = _service_account_info.from_dict(
356 info, require=['client_email'])
357 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700358
359 @classmethod
360 def from_service_account_file(cls, filename, **kwargs):
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700361 """Creates a Credentials instance from a service account .json file
362 in Google format.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700363
364 Args:
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700365 filename (str): The path to the service account .json file.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700366 kwargs: Additional arguments to pass to the constructor.
367
368 Returns:
369 google.auth.jwt.Credentials: The constructed credentials.
370 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700371 info, signer = _service_account_info.from_filename(
372 filename, require=['client_email'])
373 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700374
375 def with_claims(self, issuer=None, subject=None, audience=None,
376 additional_claims=None):
377 """Returns a copy of these credentials with modified claims.
378
379 Args:
380 issuer (str): The `iss` claim. If unspecified the current issuer
381 claim will be used.
382 subject (str): The `sub` claim. If unspecified the current subject
383 claim will be used.
384 audience (str): the `aud` claim. If not specified, a new
385 JWT will be generated for every request and will use
386 the request URI as the audience.
387 additional_claims (Mapping[str, str]): Any additional claims for
388 the JWT payload. This will be merged with the current
389 additional claims.
390
391 Returns:
392 google.auth.jwt.Credentials: A new credentials instance.
393 """
394 return Credentials(
395 self._signer,
396 issuer=issuer if issuer is not None else self._issuer,
397 subject=subject if subject is not None else self._subject,
398 audience=audience if audience is not None else self._audience,
399 additional_claims=self._additional_claims.copy().update(
400 additional_claims or {}))
401
402 def _make_jwt(self, audience=None):
403 """Make a signed JWT.
404
405 Args:
406 audience (str): Overrides the instance's current audience claim.
407
408 Returns:
Danny Hermes48c85f72016-11-08 09:30:44 -0800409 Tuple[bytes, datetime]: The encoded JWT and the expiration.
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -0700410 """
411 now = _helpers.utcnow()
412 lifetime = datetime.timedelta(seconds=self._token_lifetime)
413 expiry = now + lifetime
414
415 payload = {
416 'iss': self._issuer,
417 'sub': self._subject or self._issuer,
418 'iat': _helpers.datetime_to_secs(now),
419 'exp': _helpers.datetime_to_secs(expiry),
420 'aud': audience or self._audience,
421 }
422
423 payload.update(self._additional_claims)
424
425 jwt = encode(self._signer, payload)
426
427 return jwt, expiry
428
429 def _make_one_time_jwt(self, uri):
430 """Makes a one-off JWT with the URI as the audience.
431
432 Args:
433 uri (str): The request URI.
434
435 Returns:
436 bytes: The encoded JWT.
437 """
438 parts = urllib.parse.urlsplit(uri)
439 # Strip query string and fragment
440 audience = urllib.parse.urlunsplit(
441 (parts.scheme, parts.netloc, parts.path, None, None))
442 token, _ = self._make_jwt(audience=audience)
443 return token
444
445 def refresh(self, request):
446 """Refreshes the access token.
447
448 Args:
449 request (Any): Unused.
450 """
451 # pylint: disable=unused-argument
452 # (pylint doesn't correctly recognize overridden methods.)
453 self.token, self.expiry = self._make_jwt()
454
455 def sign_bytes(self, message):
456 """Signs the given message.
457
458 Args:
459 message (bytes): The message to sign.
460
461 Returns:
462 bytes: The message signature.
463 """
464 return self._signer.sign(message)
465
466 def before_request(self, request, method, url, headers):
467 """Performs credential-specific before request logic.
468
469 If an audience is specified it will refresh the credentials if
470 necessary. If no audience is specified it will generate a one-time
471 token for the request URI. In either case, it will set the
472 authorization header in headers to the token.
473
474 Args:
475 request (Any): Unused.
476 method (str): The request's HTTP method.
477 url (str): The request's URI.
478 headers (Mapping): The request's headers.
479 """
480 # pylint: disable=unused-argument
481 # (pylint doesn't correctly recognize overridden methods.)
482
483 # If this set of credentials has a pre-set audience, just ensure that
484 # there is a valid token and apply the auth headers.
485 if self._audience:
486 if not self.valid:
487 self.refresh(request)
488 self.apply(headers)
489 # Otherwise, generate a one-time token using the URL
490 # (without the query string and fragment) as the audience.
491 else:
492 token = self._make_one_time_jwt(url)
493 self.apply(headers, token=token)