blob: 5db1fa2c0036e5ccb96f47f0623a94ddd2fc5490 [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
46import io
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070047import json
48
Jon Wayne Parrottabcd3ed2016-10-17 11:23:47 -070049from six.moves import urllib
50
Jon Wayne Parrott54a85172016-10-17 11:27:37 -070051from google.auth import _helpers
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:
113 Tuple(str, str, str, str): header, payload, signed_section, and
114 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
321 def from_service_account_info(cls, info, **kwargs):
322 """Creates a Credentials instance from parsed service account info.
323
324 Args:
325 info (Mapping[str, str]): The service account info in Google
326 format.
327 kwargs: Additional arguments to pass to the constructor.
328
329 Returns:
330 google.auth.jwt.Credentials: The constructed credentials.
331
332 Raises:
333 ValueError: If the info is not in the expected format.
334 """
335
336 try:
337 email = info['client_email']
338 key_id = info['private_key_id']
339 private_key = info['private_key']
340 except KeyError:
341 raise ValueError(
342 'Service account info was not in the expected format.')
343
344 signer = crypt.Signer.from_string(private_key, key_id)
345
346 kwargs.setdefault('subject', email)
347 return cls(signer, issuer=email, **kwargs)
348
349 @classmethod
350 def from_service_account_file(cls, filename, **kwargs):
351 """Creates a Credentials instance from a service account json file.
352
353 Args:
354 filename (str): The path to the service account json file.
355 kwargs: Additional arguments to pass to the constructor.
356
357 Returns:
358 google.auth.jwt.Credentials: The constructed credentials.
359 """
360 with io.open(filename, 'r', encoding='utf-8') as json_file:
361 info = json.load(json_file)
362 return cls.from_service_account_info(info, **kwargs)
363
364 def with_claims(self, issuer=None, subject=None, audience=None,
365 additional_claims=None):
366 """Returns a copy of these credentials with modified claims.
367
368 Args:
369 issuer (str): The `iss` claim. If unspecified the current issuer
370 claim will be used.
371 subject (str): The `sub` claim. If unspecified the current subject
372 claim will be used.
373 audience (str): the `aud` claim. If not specified, a new
374 JWT will be generated for every request and will use
375 the request URI as the audience.
376 additional_claims (Mapping[str, str]): Any additional claims for
377 the JWT payload. This will be merged with the current
378 additional claims.
379
380 Returns:
381 google.auth.jwt.Credentials: A new credentials instance.
382 """
383 return Credentials(
384 self._signer,
385 issuer=issuer if issuer is not None else self._issuer,
386 subject=subject if subject is not None else self._subject,
387 audience=audience if audience is not None else self._audience,
388 additional_claims=self._additional_claims.copy().update(
389 additional_claims or {}))
390
391 def _make_jwt(self, audience=None):
392 """Make a signed JWT.
393
394 Args:
395 audience (str): Overrides the instance's current audience claim.
396
397 Returns:
398 Tuple(bytes, datetime): The encoded JWT and the expiration.
399 """
400 now = _helpers.utcnow()
401 lifetime = datetime.timedelta(seconds=self._token_lifetime)
402 expiry = now + lifetime
403
404 payload = {
405 'iss': self._issuer,
406 'sub': self._subject or self._issuer,
407 'iat': _helpers.datetime_to_secs(now),
408 'exp': _helpers.datetime_to_secs(expiry),
409 'aud': audience or self._audience,
410 }
411
412 payload.update(self._additional_claims)
413
414 jwt = encode(self._signer, payload)
415
416 return jwt, expiry
417
418 def _make_one_time_jwt(self, uri):
419 """Makes a one-off JWT with the URI as the audience.
420
421 Args:
422 uri (str): The request URI.
423
424 Returns:
425 bytes: The encoded JWT.
426 """
427 parts = urllib.parse.urlsplit(uri)
428 # Strip query string and fragment
429 audience = urllib.parse.urlunsplit(
430 (parts.scheme, parts.netloc, parts.path, None, None))
431 token, _ = self._make_jwt(audience=audience)
432 return token
433
434 def refresh(self, request):
435 """Refreshes the access token.
436
437 Args:
438 request (Any): Unused.
439 """
440 # pylint: disable=unused-argument
441 # (pylint doesn't correctly recognize overridden methods.)
442 self.token, self.expiry = self._make_jwt()
443
444 def sign_bytes(self, message):
445 """Signs the given message.
446
447 Args:
448 message (bytes): The message to sign.
449
450 Returns:
451 bytes: The message signature.
452 """
453 return self._signer.sign(message)
454
455 def before_request(self, request, method, url, headers):
456 """Performs credential-specific before request logic.
457
458 If an audience is specified it will refresh the credentials if
459 necessary. If no audience is specified it will generate a one-time
460 token for the request URI. In either case, it will set the
461 authorization header in headers to the token.
462
463 Args:
464 request (Any): Unused.
465 method (str): The request's HTTP method.
466 url (str): The request's URI.
467 headers (Mapping): The request's headers.
468 """
469 # pylint: disable=unused-argument
470 # (pylint doesn't correctly recognize overridden methods.)
471
472 # If this set of credentials has a pre-set audience, just ensure that
473 # there is a valid token and apply the auth headers.
474 if self._audience:
475 if not self.valid:
476 self.refresh(request)
477 self.apply(headers)
478 # Otherwise, generate a one-time token using the URL
479 # (without the query string and fragment) as the audience.
480 else:
481 token = self._make_one_time_jwt(url)
482 self.apply(headers, token=token)