blob: c60c5654644390e8f011e2c3af2f46faaa5e01ed [file] [log] [blame]
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -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"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
16
17This module implements the JWT Profile for OAuth 2.0 Authorization Grants
18as defined by `RFC 7523`_ with particular support for how this RFC is
19implemented in Google's infrastructure. Google refers to these credentials
20as *Service Accounts*.
21
22Service accounts are used for server-to-server communication, such as
23interactions between a web application server and a Google service. The
24service account belongs to your application instead of to an individual end
25user. In contrast to other OAuth 2.0 profiles, no users are involved and your
26application "acts" as the service account.
27
28Typically an application uses a service account when the application uses
29Google APIs to work with its own data rather than a user's data. For example,
30an application that uses Google Cloud Datastore for data persistence would use
31a service account to authenticate its calls to the Google Cloud Datastore API.
32However, an application that needs to access a user's Drive documents would
33use the normal OAuth 2.0 profile.
34
35Additionally, Google Apps domain administrators can grant service accounts
36`domain-wide delegation`_ authority to access user data on behalf of users in
37the domain.
38
39This profile uses a JWT to acquire an OAuth 2.0 access token. The JWT is used
40in place of the usual authorization token returned during the standard
41OAuth 2.0 Authorization Code grant. The JWT is only used for this purpose, as
42the acquired access token is used as the bearer token when making requests
43using these credentials.
44
45This profile differs from normal OAuth 2.0 profile because no user consent
46step is required. The use of the private key allows this profile to assert
47identity directly.
48
49This profile also differs from the :mod:`google.auth.jwt` authentication
50because the JWT credentials use the JWT directly as the bearer token. This
51profile instead only uses the JWT to obtain an OAuth 2.0 access token. The
52obtained OAuth 2.0 access token is used as the bearer token.
53
54Domain-wide delegation
55----------------------
56
57Domain-wide delegation allows a service account to access user data on
58behalf of any user in a Google Apps domain without consent from the user.
59For example, an application that uses the Google Calendar API to add events to
60the calendars of all users in a Google Apps domain would use a service account
61to access the Google Calendar API on behalf of users.
62
63The Google Apps administrator must explicitly authorize the service account to
64do this. This authorization step is referred to as "delegating domain-wide
65authority" to a service account.
66
67You can use domain-wise delegation by creating a set of credentials with a
68specific subject using :meth:`~Credentials.with_subject`.
69
70.. _RFC 7523: https://tools.ietf.org/html/rfc7523
71"""
72
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -070073import copy
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -070074import datetime
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -070075
76from google.auth import _helpers
Jon Wayne Parrott807032c2016-10-18 09:38:26 -070077from google.auth import _service_account_info
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -070078from google.auth import credentials
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -070079from google.auth import jwt
80from google.oauth2 import _client
81
Albert-Jan Nijburgb7b48f12017-11-08 20:15:18 +000082_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -070083
84
85class Credentials(credentials.Signing,
86 credentials.Scoped,
87 credentials.Credentials):
88 """Service account credentials
89
90 Usually, you'll create these credentials with one of the helper
91 constructors. To create credentials using a Google service account
92 private key JSON file::
93
94 credentials = service_account.Credentials.from_service_account_file(
95 'service-account.json')
96
97 Or if you already have the service account file loaded::
98
99 service_account_info = json.load(open('service_account.json'))
100 credentials = service_account.Credentials.from_service_account_info(
101 service_account_info)
102
103 Both helper methods pass on arguments to the constructor, so you can
104 specify additional scopes and a subject if necessary::
105
106 credentials = service_account.Credentials.from_service_account_file(
107 'service-account.json',
108 scopes=['email'],
109 subject='user@example.com')
110
111 The credentials are considered immutable. If you want to modify the scopes
112 or the subject used for delegation, use :meth:`with_scopes` or
113 :meth:`with_subject`::
114
115 scoped_credentials = credentials.with_scopes(['email'])
116 delegated_credentials = credentials.with_subject(subject)
117 """
118
119 def __init__(self, signer, service_account_email, token_uri, scopes=None,
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700120 subject=None, project_id=None, additional_claims=None):
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700121 """
122 Args:
123 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
124 service_account_email (str): The service account's email.
125 scopes (Sequence[str]): Scopes to request during the authorization
126 grant.
127 token_uri (str): The OAuth 2.0 Token URI.
128 subject (str): For domain-wide delegation, the email address of the
129 user to for which to request delegated access.
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700130 project_id (str): Project ID associated with the service account
131 credential.
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700132 additional_claims (Mapping[str, str]): Any additional claims for
133 the JWT assertion used in the authorization grant.
134
135 .. note:: Typically one of the helper constructors
136 :meth:`from_service_account_file` or
137 :meth:`from_service_account_info` are used instead of calling the
138 constructor directly.
139 """
140 super(Credentials, self).__init__()
141
142 self._scopes = scopes
143 self._signer = signer
144 self._service_account_email = service_account_email
145 self._subject = subject
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700146 self._project_id = project_id
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700147 self._token_uri = token_uri
148
149 if additional_claims is not None:
150 self._additional_claims = additional_claims
151 else:
152 self._additional_claims = {}
153
154 @classmethod
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700155 def _from_signer_and_info(cls, signer, info, **kwargs):
156 """Creates a Credentials instance from a signer and service account
157 info.
158
159 Args:
160 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
161 info (Mapping[str, str]): The service account info.
162 kwargs: Additional arguments to pass to the constructor.
163
164 Returns:
165 google.auth.jwt.Credentials: The constructed credentials.
166
167 Raises:
168 ValueError: If the info is not in the expected format.
169 """
170 return cls(
171 signer,
172 service_account_email=info['client_email'],
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700173 token_uri=info['token_uri'],
174 project_id=info.get('project_id'), **kwargs)
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700175
176 @classmethod
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700177 def from_service_account_info(cls, info, **kwargs):
178 """Creates a Credentials instance from parsed service account info.
179
180 Args:
181 info (Mapping[str, str]): The service account info in Google
182 format.
183 kwargs: Additional arguments to pass to the constructor.
184
185 Returns:
186 google.auth.service_account.Credentials: The constructed
187 credentials.
188
189 Raises:
190 ValueError: If the info is not in the expected format.
191 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700192 signer = _service_account_info.from_dict(
193 info, require=['client_email', 'token_uri'])
194 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700195
196 @classmethod
197 def from_service_account_file(cls, filename, **kwargs):
198 """Creates a Credentials instance from a service account json file.
199
200 Args:
201 filename (str): The path to the service account json file.
202 kwargs: Additional arguments to pass to the constructor.
203
204 Returns:
205 google.auth.service_account.Credentials: The constructed
206 credentials.
207 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700208 info, signer = _service_account_info.from_filename(
209 filename, require=['client_email', 'token_uri'])
210 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700211
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700212 @property
Jon Wayne Parrott61ffb052016-11-08 09:30:30 -0800213 def service_account_email(self):
214 """The service account email."""
215 return self._service_account_email
216
217 @property
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700218 def project_id(self):
219 """Project ID associated with this credential."""
220 return self._project_id
221
222 @property
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700223 def requires_scopes(self):
224 """Checks if the credentials requires scopes.
225
226 Returns:
227 bool: True if there are no scopes set otherwise False.
228 """
229 return True if not self._scopes else False
230
231 @_helpers.copy_docstring(credentials.Scoped)
232 def with_scopes(self, scopes):
Christophe Tatonb649b432018-02-08 14:12:23 -0800233 return self.__class__(
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700234 self._signer,
235 service_account_email=self._service_account_email,
236 scopes=scopes,
237 token_uri=self._token_uri,
238 subject=self._subject,
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700239 project_id=self._project_id,
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700240 additional_claims=self._additional_claims.copy())
241
242 def with_subject(self, subject):
243 """Create a copy of these credentials with the specified subject.
244
245 Args:
246 subject (str): The subject claim.
247
248 Returns:
249 google.auth.service_account.Credentials: A new credentials
250 instance.
251 """
Christophe Tatonb649b432018-02-08 14:12:23 -0800252 return self.__class__(
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700253 self._signer,
254 service_account_email=self._service_account_email,
255 scopes=self._scopes,
256 token_uri=self._token_uri,
257 subject=subject,
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700258 project_id=self._project_id,
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700259 additional_claims=self._additional_claims.copy())
260
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700261 def with_claims(self, additional_claims):
262 """Returns a copy of these credentials with modified claims.
263
264 Args:
265 additional_claims (Mapping[str, str]): Any additional claims for
266 the JWT payload. This will be merged with the current
267 additional claims.
268
269 Returns:
270 google.auth.service_account.Credentials: A new credentials
271 instance.
272 """
273 new_additional_claims = copy.deepcopy(self._additional_claims)
274 new_additional_claims.update(additional_claims or {})
275
Christophe Tatonb649b432018-02-08 14:12:23 -0800276 return self.__class__(
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700277 self._signer,
278 service_account_email=self._service_account_email,
279 scopes=self._scopes,
280 token_uri=self._token_uri,
281 subject=self._subject,
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700282 project_id=self._project_id,
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700283 additional_claims=new_additional_claims)
284
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700285 def _make_authorization_grant_assertion(self):
286 """Create the OAuth 2.0 assertion.
287
288 This assertion is used during the OAuth 2.0 grant to acquire an
289 access token.
290
291 Returns:
292 bytes: The authorization grant assertion.
293 """
294 now = _helpers.utcnow()
295 lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
296 expiry = now + lifetime
297
298 payload = {
299 'iat': _helpers.datetime_to_secs(now),
300 'exp': _helpers.datetime_to_secs(expiry),
301 # The issuer must be the service account email.
302 'iss': self._service_account_email,
303 # The audience must be the auth token endpoint's URI
304 'aud': self._token_uri,
305 'scope': _helpers.scopes_to_string(self._scopes or ())
306 }
307
308 payload.update(self._additional_claims)
309
310 # The subject can be a user email for domain-wide delegation.
311 if self._subject:
312 payload.setdefault('sub', self._subject)
313
314 token = jwt.encode(self._signer, payload)
315
316 return token
317
318 @_helpers.copy_docstring(credentials.Credentials)
319 def refresh(self, request):
320 assertion = self._make_authorization_grant_assertion()
321 access_token, expiry, _ = _client.jwt_grant(
322 request, self._token_uri, assertion)
323 self.token = access_token
324 self.expiry = expiry
325
326 @_helpers.copy_docstring(credentials.Signing)
327 def sign_bytes(self, message):
328 return self._signer.sign(message)
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800329
330 @property
331 @_helpers.copy_docstring(credentials.Signing)
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800332 def signer(self):
333 return self._signer
334
335 @property
336 @_helpers.copy_docstring(credentials.Signing)
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800337 def signer_email(self):
338 return self._service_account_email
Christophe Tatonb649b432018-02-08 14:12:23 -0800339
340
341class IDTokenCredentials(credentials.Signing, credentials.Credentials):
342 """Open ID Connect ID Token-based service account credentials.
343
344 These credentials are largely similar to :class:`.Credentials`, but instead
345 of using an OAuth 2.0 Access Token as the bearer token, they use an Open
346 ID Connect ID Token as the bearer token. These credentials are useful when
347 communicating to services that require ID Tokens and can not accept access
348 tokens.
349
350 Usually, you'll create these credentials with one of the helper
351 constructors. To create credentials using a Google service account
352 private key JSON file::
353
354 credentials = (
355 service_account.IDTokenCredentials.from_service_account_file(
356 'service-account.json'))
357
358 Or if you already have the service account file loaded::
359
360 service_account_info = json.load(open('service_account.json'))
361 credentials = (
362 service_account.IDTokenCredentials.from_service_account_info(
363 service_account_info))
364
365 Both helper methods pass on arguments to the constructor, so you can
366 specify additional scopes and a subject if necessary::
367
368 credentials = (
369 service_account.IDTokenCredentials.from_service_account_file(
370 'service-account.json',
371 scopes=['email'],
372 subject='user@example.com'))
373`
374 The credentials are considered immutable. If you want to modify the scopes
375 or the subject used for delegation, use :meth:`with_scopes` or
376 :meth:`with_subject`::
377
378 scoped_credentials = credentials.with_scopes(['email'])
379 delegated_credentials = credentials.with_subject(subject)
380
381 """
382 def __init__(self, signer, service_account_email, token_uri,
383 target_audience, additional_claims=None):
384 """
385 Args:
386 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
387 service_account_email (str): The service account's email.
388 token_uri (str): The OAuth 2.0 Token URI.
389 target_audience (str): The intended audience for these credentials,
390 used when requesting the ID Token. The ID Token's ``aud`` claim
391 will be set to this string.
392 additional_claims (Mapping[str, str]): Any additional claims for
393 the JWT assertion used in the authorization grant.
394
395 .. note:: Typically one of the helper constructors
396 :meth:`from_service_account_file` or
397 :meth:`from_service_account_info` are used instead of calling the
398 constructor directly.
399 """
400 super(IDTokenCredentials, self).__init__()
401 self._signer = signer
402 self._service_account_email = service_account_email
403 self._token_uri = token_uri
404 self._target_audience = target_audience
405
406 if additional_claims is not None:
407 self._additional_claims = additional_claims
408 else:
409 self._additional_claims = {}
410
411 @classmethod
412 def _from_signer_and_info(cls, signer, info, **kwargs):
413 """Creates a credentials instance from a signer and service account
414 info.
415
416 Args:
417 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
418 info (Mapping[str, str]): The service account info.
419 kwargs: Additional arguments to pass to the constructor.
420
421 Returns:
422 google.auth.jwt.IDTokenCredentials: The constructed credentials.
423
424 Raises:
425 ValueError: If the info is not in the expected format.
426 """
427 kwargs.setdefault('service_account_email', info['client_email'])
428 kwargs.setdefault('token_uri', info['token_uri'])
429 return cls(signer, **kwargs)
430
431 @classmethod
432 def from_service_account_info(cls, info, **kwargs):
433 """Creates a credentials instance from parsed service account info.
434
435 Args:
436 info (Mapping[str, str]): The service account info in Google
437 format.
438 kwargs: Additional arguments to pass to the constructor.
439
440 Returns:
441 google.auth.service_account.IDTokenCredentials: The constructed
442 credentials.
443
444 Raises:
445 ValueError: If the info is not in the expected format.
446 """
447 signer = _service_account_info.from_dict(
448 info, require=['client_email', 'token_uri'])
449 return cls._from_signer_and_info(signer, info, **kwargs)
450
451 @classmethod
452 def from_service_account_file(cls, filename, **kwargs):
453 """Creates a credentials instance from a service account json file.
454
455 Args:
456 filename (str): The path to the service account json file.
457 kwargs: Additional arguments to pass to the constructor.
458
459 Returns:
460 google.auth.service_account.IDTokenCredentials: The constructed
461 credentials.
462 """
463 info, signer = _service_account_info.from_filename(
464 filename, require=['client_email', 'token_uri'])
465 return cls._from_signer_and_info(signer, info, **kwargs)
466
467 def with_target_audience(self, target_audience):
468 """Create a copy of these credentials with the specified target
469 audience.
470
471 Args:
472 target_audience (str): The intended audience for these credentials,
473 used when requesting the ID Token.
474
475 Returns:
476 google.auth.service_account.IDTokenCredentials: A new credentials
477 instance.
478 """
479 return self.__class__(
480 self._signer,
481 service_account_email=self._service_account_email,
482 token_uri=self._token_uri,
483 target_audience=target_audience,
484 additional_claims=self._additional_claims.copy())
485
486 def _make_authorization_grant_assertion(self):
487 """Create the OAuth 2.0 assertion.
488
489 This assertion is used during the OAuth 2.0 grant to acquire an
490 ID token.
491
492 Returns:
493 bytes: The authorization grant assertion.
494 """
495 now = _helpers.utcnow()
496 lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
497 expiry = now + lifetime
498
499 payload = {
500 'iat': _helpers.datetime_to_secs(now),
501 'exp': _helpers.datetime_to_secs(expiry),
502 # The issuer must be the service account email.
503 'iss': self.service_account_email,
504 # The audience must be the auth token endpoint's URI
505 'aud': self._token_uri,
506 # The target audience specifies which service the ID token is
507 # intended for.
508 'target_audience': self._target_audience
509 }
510
511 payload.update(self._additional_claims)
512
513 token = jwt.encode(self._signer, payload)
514
515 return token
516
517 @_helpers.copy_docstring(credentials.Credentials)
518 def refresh(self, request):
519 assertion = self._make_authorization_grant_assertion()
520 access_token, expiry, _ = _client.id_token_jwt_grant(
521 request, self._token_uri, assertion)
522 self.token = access_token
523 self.expiry = expiry
524
525 @property
526 def service_account_email(self):
527 """The service account email."""
528 return self._service_account_email
529
530 @_helpers.copy_docstring(credentials.Signing)
531 def sign_bytes(self, message):
532 return self._signer.sign(message)
533
534 @property
535 @_helpers.copy_docstring(credentials.Signing)
536 def signer(self):
537 return self._signer
538
539 @property
540 @_helpers.copy_docstring(credentials.Signing)
541 def signer_email(self):
542 return self._service_account_email