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