blob: f68fc4b4df9d0f2cc6897089b97601104ed2e094 [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
45import json
46
47from google.auth import crypt
48from google.auth import _helpers
49
50
51_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in sections
52_CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
53
54
55def encode(signer, payload, header=None, key_id=None):
56 """Make a signed JWT.
57
58 Args:
59 signer (google.auth.crypt.Signer): The signer used to sign the JWT.
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -070060 payload (Mapping[str, str]): The JWT payload.
61 header (Mapping[str, str]): Additional JWT header payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -070062 key_id (str): The key id to add to the JWT header. If the
63 signer has a key id it will be used as the default. If this is
64 specified it will override the signer's key id.
65
66 Returns:
67 bytes: The encoded JWT.
68 """
69 if header is None:
70 header = {}
71
72 if key_id is None:
73 key_id = signer.key_id
74
75 header.update({'typ': 'JWT', 'alg': 'RS256'})
76
77 if key_id is not None:
78 header['kid'] = key_id
79
80 segments = [
81 base64.urlsafe_b64encode(json.dumps(header).encode('utf-8')),
82 base64.urlsafe_b64encode(json.dumps(payload).encode('utf-8')),
83 ]
84
85 signing_input = b'.'.join(segments)
86 signature = signer.sign(signing_input)
87 segments.append(base64.urlsafe_b64encode(signature))
88
89 return b'.'.join(segments)
90
91
92def _decode_jwt_segment(encoded_section):
93 """Decodes a single JWT segment."""
94 section_bytes = base64.urlsafe_b64decode(encoded_section)
95 try:
96 return json.loads(section_bytes.decode('utf-8'))
97 except ValueError:
98 raise ValueError('Can\'t parse segment: {0}'.format(section_bytes))
99
100
101def _unverified_decode(token):
102 """Decodes a token and does no verification.
103
104 Args:
105 token (Union[str, bytes]): The encoded JWT.
106
107 Returns:
108 Tuple(str, str, str, str): header, payload, signed_section, and
109 signature.
110
111 Raises:
112 ValueError: if there are an incorrect amount of segments in the token.
113 """
114 token = _helpers.to_bytes(token)
115
116 if token.count(b'.') != 2:
117 raise ValueError(
118 'Wrong number of segments in token: {0}'.format(token))
119
120 encoded_header, encoded_payload, signature = token.split(b'.')
121 signed_section = encoded_header + b'.' + encoded_payload
122 signature = base64.urlsafe_b64decode(signature)
123
124 # Parse segments
125 header = _decode_jwt_segment(encoded_header)
126 payload = _decode_jwt_segment(encoded_payload)
127
128 return header, payload, signed_section, signature
129
130
131def decode_header(token):
132 """Return the decoded header of a token.
133
134 No verification is done. This is useful to extract the key id from
135 the header in order to acquire the appropriate certificate to verify
136 the token.
137
138 Args:
139 token (Union[str, bytes]): the encoded JWT.
140
141 Returns:
142 Mapping: The decoded JWT header.
143 """
144 header, _, _, _ = _unverified_decode(token)
145 return header
146
147
148def _verify_iat_and_exp(payload):
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700149 """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700150 payload.
151
152 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700153 payload (Mapping[str, str]): The JWT payload.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700154
155 Raises:
156 ValueError: if any checks failed.
157 """
158 now = _helpers.datetime_to_secs(_helpers.utcnow())
159
160 # Make sure the iat and exp claims are present
161 for key in ('iat', 'exp'):
162 if key not in payload:
163 raise ValueError(
164 'Token does not contain required claim {}'.format(key))
165
166 # Make sure the token wasn't issued in the future
167 iat = payload['iat']
168 earliest = iat - _CLOCK_SKEW_SECS
169 if now < earliest:
170 raise ValueError('Token used too early, {} < {}'.format(now, iat))
171
172 # Make sure the token wasn't issue in the past
173 exp = payload['exp']
174 latest = exp + _CLOCK_SKEW_SECS
175 if latest < now:
176 raise ValueError('Token expired, {} < {}'.format(latest, now))
177
178
179def decode(token, certs=None, verify=True, audience=None):
180 """Decode and verify a JWT.
181
182 Args:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700183 token (str): The encoded JWT.
184 certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
185 certificate used to validate the JWT signatyre. If bytes or string,
186 it must the the public key certificate in PEM format. If a mapping,
187 it must be a mapping of key IDs to public key certificates in PEM
188 format. The mapping must contain the same key ID that's specified
189 in the token's header.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700190 verify (bool): Whether to perform signature and claim validation.
191 Verification is done by default.
192 audience (str): The audience claim, 'aud', that this JWT should
193 contain. If None then the JWT's 'aud' parameter is not verified.
194
195 Returns:
Jon Wayne Parrott7eeab7d2016-10-12 15:02:37 -0700196 Mapping[str, str]: The deserialized JSON payload in the JWT.
Jon Wayne Parrott5824ad82016-10-06 09:27:44 -0700197
198 Raises:
199 ValueError: if any verification checks failed.
200 """
201 header, payload, signed_section, signature = _unverified_decode(token)
202
203 if not verify:
204 return payload
205
206 # If certs is specified as a dictionary of key IDs to certificates, then
207 # use the certificate identified by the key ID in the token header.
208 if isinstance(certs, collections.Mapping):
209 key_id = header.get('kid')
210 if key_id:
211 if key_id not in certs:
212 raise ValueError(
213 'Certificate for key id {} not found.'.format(key_id))
214 certs_to_check = [certs[key_id]]
215 # If there's no key id in the header, check against all of the certs.
216 else:
217 certs_to_check = certs.values()
218 else:
219 certs_to_check = certs
220
221 # Verify that the signature matches the message.
222 if not crypt.verify_signature(signed_section, signature, certs_to_check):
223 raise ValueError('Could not verify token signature.')
224
225 # Verify the issued at and created times in the payload.
226 _verify_iat_and_exp(payload)
227
228 # Check audience.
229 if audience is not None:
230 claim_audience = payload.get('aud')
231 if audience != claim_audience:
232 raise ValueError(
233 'Token has wrong audience {}, expected {}'.format(
234 claim_audience, audience))
235
236 return payload