blob: 17fdd516d208f84e5a79dfe57e5fa968967b2d85 [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
Bu Sun Kim9eec0912019-10-21 17:04:21 -070085class Credentials(credentials.Signing, credentials.Scoped, credentials.Credentials):
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -070086 """Service account credentials
87
88 Usually, you'll create these credentials with one of the helper
89 constructors. To create credentials using a Google service account
90 private key JSON file::
91
92 credentials = service_account.Credentials.from_service_account_file(
93 'service-account.json')
94
95 Or if you already have the service account file loaded::
96
97 service_account_info = json.load(open('service_account.json'))
98 credentials = service_account.Credentials.from_service_account_info(
99 service_account_info)
100
101 Both helper methods pass on arguments to the constructor, so you can
102 specify additional scopes and a subject if necessary::
103
104 credentials = service_account.Credentials.from_service_account_file(
105 'service-account.json',
106 scopes=['email'],
107 subject='user@example.com')
108
109 The credentials are considered immutable. If you want to modify the scopes
110 or the subject used for delegation, use :meth:`with_scopes` or
111 :meth:`with_subject`::
112
113 scoped_credentials = credentials.with_scopes(['email'])
114 delegated_credentials = credentials.with_subject(subject)
115 """
116
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700117 def __init__(
118 self,
119 signer,
120 service_account_email,
121 token_uri,
122 scopes=None,
123 subject=None,
124 project_id=None,
125 additional_claims=None,
126 ):
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700127 """
128 Args:
129 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
130 service_account_email (str): The service account's email.
131 scopes (Sequence[str]): Scopes to request during the authorization
132 grant.
133 token_uri (str): The OAuth 2.0 Token URI.
134 subject (str): For domain-wide delegation, the email address of the
135 user to for which to request delegated access.
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700136 project_id (str): Project ID associated with the service account
137 credential.
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700138 additional_claims (Mapping[str, str]): Any additional claims for
139 the JWT assertion used in the authorization grant.
140
141 .. note:: Typically one of the helper constructors
142 :meth:`from_service_account_file` or
143 :meth:`from_service_account_info` are used instead of calling the
144 constructor directly.
145 """
146 super(Credentials, self).__init__()
147
148 self._scopes = scopes
149 self._signer = signer
150 self._service_account_email = service_account_email
151 self._subject = subject
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700152 self._project_id = project_id
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700153 self._token_uri = token_uri
154
155 if additional_claims is not None:
156 self._additional_claims = additional_claims
157 else:
158 self._additional_claims = {}
159
160 @classmethod
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700161 def _from_signer_and_info(cls, signer, info, **kwargs):
162 """Creates a Credentials instance from a signer and service account
163 info.
164
165 Args:
166 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
167 info (Mapping[str, str]): The service account info.
168 kwargs: Additional arguments to pass to the constructor.
169
170 Returns:
171 google.auth.jwt.Credentials: The constructed credentials.
172
173 Raises:
174 ValueError: If the info is not in the expected format.
175 """
176 return cls(
177 signer,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700178 service_account_email=info["client_email"],
179 token_uri=info["token_uri"],
180 project_id=info.get("project_id"),
181 **kwargs
182 )
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700183
184 @classmethod
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700185 def from_service_account_info(cls, info, **kwargs):
186 """Creates a Credentials instance from parsed service account info.
187
188 Args:
189 info (Mapping[str, str]): The service account info in Google
190 format.
191 kwargs: Additional arguments to pass to the constructor.
192
193 Returns:
194 google.auth.service_account.Credentials: The constructed
195 credentials.
196
197 Raises:
198 ValueError: If the info is not in the expected format.
199 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700200 signer = _service_account_info.from_dict(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700201 info, require=["client_email", "token_uri"]
202 )
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700203 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700204
205 @classmethod
206 def from_service_account_file(cls, filename, **kwargs):
207 """Creates a Credentials instance from a service account json file.
208
209 Args:
210 filename (str): The path to the service account json file.
211 kwargs: Additional arguments to pass to the constructor.
212
213 Returns:
214 google.auth.service_account.Credentials: The constructed
215 credentials.
216 """
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700217 info, signer = _service_account_info.from_filename(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700218 filename, require=["client_email", "token_uri"]
219 )
Jon Wayne Parrott807032c2016-10-18 09:38:26 -0700220 return cls._from_signer_and_info(signer, info, **kwargs)
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700221
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700222 @property
Jon Wayne Parrott61ffb052016-11-08 09:30:30 -0800223 def service_account_email(self):
224 """The service account email."""
225 return self._service_account_email
226
227 @property
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700228 def project_id(self):
229 """Project ID associated with this credential."""
230 return self._project_id
231
232 @property
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700233 def requires_scopes(self):
234 """Checks if the credentials requires scopes.
235
236 Returns:
237 bool: True if there are no scopes set otherwise False.
238 """
239 return True if not self._scopes else False
240
241 @_helpers.copy_docstring(credentials.Scoped)
242 def with_scopes(self, scopes):
Christophe Tatonb649b432018-02-08 14:12:23 -0800243 return self.__class__(
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700244 self._signer,
245 service_account_email=self._service_account_email,
246 scopes=scopes,
247 token_uri=self._token_uri,
248 subject=self._subject,
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700249 project_id=self._project_id,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700250 additional_claims=self._additional_claims.copy(),
251 )
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700252
253 def with_subject(self, subject):
254 """Create a copy of these credentials with the specified subject.
255
256 Args:
257 subject (str): The subject claim.
258
259 Returns:
260 google.auth.service_account.Credentials: A new credentials
261 instance.
262 """
Christophe Tatonb649b432018-02-08 14:12:23 -0800263 return self.__class__(
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700264 self._signer,
265 service_account_email=self._service_account_email,
266 scopes=self._scopes,
267 token_uri=self._token_uri,
268 subject=subject,
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700269 project_id=self._project_id,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700270 additional_claims=self._additional_claims.copy(),
271 )
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700272
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700273 def with_claims(self, additional_claims):
274 """Returns a copy of these credentials with modified claims.
275
276 Args:
277 additional_claims (Mapping[str, str]): Any additional claims for
278 the JWT payload. This will be merged with the current
279 additional claims.
280
281 Returns:
282 google.auth.service_account.Credentials: A new credentials
283 instance.
284 """
285 new_additional_claims = copy.deepcopy(self._additional_claims)
286 new_additional_claims.update(additional_claims or {})
287
Christophe Tatonb649b432018-02-08 14:12:23 -0800288 return self.__class__(
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700289 self._signer,
290 service_account_email=self._service_account_email,
291 scopes=self._scopes,
292 token_uri=self._token_uri,
293 subject=self._subject,
Hiranya Jayathilaka6a3f0ec2017-08-10 09:11:02 -0700294 project_id=self._project_id,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700295 additional_claims=new_additional_claims,
296 )
Jon Wayne Parrott75c78b22017-03-23 13:14:53 -0700297
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700298 def _make_authorization_grant_assertion(self):
299 """Create the OAuth 2.0 assertion.
300
301 This assertion is used during the OAuth 2.0 grant to acquire an
302 access token.
303
304 Returns:
305 bytes: The authorization grant assertion.
306 """
307 now = _helpers.utcnow()
308 lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
309 expiry = now + lifetime
310
311 payload = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700312 "iat": _helpers.datetime_to_secs(now),
313 "exp": _helpers.datetime_to_secs(expiry),
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700314 # The issuer must be the service account email.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700315 "iss": self._service_account_email,
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700316 # The audience must be the auth token endpoint's URI
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700317 "aud": self._token_uri,
318 "scope": _helpers.scopes_to_string(self._scopes or ()),
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700319 }
320
321 payload.update(self._additional_claims)
322
323 # The subject can be a user email for domain-wide delegation.
324 if self._subject:
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700325 payload.setdefault("sub", self._subject)
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700326
327 token = jwt.encode(self._signer, payload)
328
329 return token
330
331 @_helpers.copy_docstring(credentials.Credentials)
332 def refresh(self, request):
333 assertion = self._make_authorization_grant_assertion()
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700334 access_token, expiry, _ = _client.jwt_grant(request, self._token_uri, assertion)
Jon Wayne Parrottab9eba32016-10-17 10:49:57 -0700335 self.token = access_token
336 self.expiry = expiry
337
338 @_helpers.copy_docstring(credentials.Signing)
339 def sign_bytes(self, message):
340 return self._signer.sign(message)
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800341
342 @property
343 @_helpers.copy_docstring(credentials.Signing)
Jon Wayne Parrottd7221672017-02-16 09:05:11 -0800344 def signer(self):
345 return self._signer
346
347 @property
348 @_helpers.copy_docstring(credentials.Signing)
Jon Wayne Parrott4c883f02016-12-02 14:26:33 -0800349 def signer_email(self):
350 return self._service_account_email
Christophe Tatonb649b432018-02-08 14:12:23 -0800351
352
353class IDTokenCredentials(credentials.Signing, credentials.Credentials):
354 """Open ID Connect ID Token-based service account credentials.
355
356 These credentials are largely similar to :class:`.Credentials`, but instead
357 of using an OAuth 2.0 Access Token as the bearer token, they use an Open
358 ID Connect ID Token as the bearer token. These credentials are useful when
359 communicating to services that require ID Tokens and can not accept access
360 tokens.
361
362 Usually, you'll create these credentials with one of the helper
363 constructors. To create credentials using a Google service account
364 private key JSON file::
365
366 credentials = (
367 service_account.IDTokenCredentials.from_service_account_file(
368 'service-account.json'))
369
370 Or if you already have the service account file loaded::
371
372 service_account_info = json.load(open('service_account.json'))
373 credentials = (
374 service_account.IDTokenCredentials.from_service_account_info(
375 service_account_info))
376
377 Both helper methods pass on arguments to the constructor, so you can
378 specify additional scopes and a subject if necessary::
379
380 credentials = (
381 service_account.IDTokenCredentials.from_service_account_file(
382 'service-account.json',
383 scopes=['email'],
384 subject='user@example.com'))
385`
386 The credentials are considered immutable. If you want to modify the scopes
387 or the subject used for delegation, use :meth:`with_scopes` or
388 :meth:`with_subject`::
389
390 scoped_credentials = credentials.with_scopes(['email'])
391 delegated_credentials = credentials.with_subject(subject)
392
393 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700394
395 def __init__(
396 self,
397 signer,
398 service_account_email,
399 token_uri,
400 target_audience,
401 additional_claims=None,
402 ):
Christophe Tatonb649b432018-02-08 14:12:23 -0800403 """
404 Args:
405 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
406 service_account_email (str): The service account's email.
407 token_uri (str): The OAuth 2.0 Token URI.
408 target_audience (str): The intended audience for these credentials,
409 used when requesting the ID Token. The ID Token's ``aud`` claim
410 will be set to this string.
411 additional_claims (Mapping[str, str]): Any additional claims for
412 the JWT assertion used in the authorization grant.
413
414 .. note:: Typically one of the helper constructors
415 :meth:`from_service_account_file` or
416 :meth:`from_service_account_info` are used instead of calling the
417 constructor directly.
418 """
419 super(IDTokenCredentials, self).__init__()
420 self._signer = signer
421 self._service_account_email = service_account_email
422 self._token_uri = token_uri
423 self._target_audience = target_audience
424
425 if additional_claims is not None:
426 self._additional_claims = additional_claims
427 else:
428 self._additional_claims = {}
429
430 @classmethod
431 def _from_signer_and_info(cls, signer, info, **kwargs):
432 """Creates a credentials instance from a signer and service account
433 info.
434
435 Args:
436 signer (google.auth.crypt.Signer): The signer used to sign JWTs.
437 info (Mapping[str, str]): The service account info.
438 kwargs: Additional arguments to pass to the constructor.
439
440 Returns:
441 google.auth.jwt.IDTokenCredentials: The constructed credentials.
442
443 Raises:
444 ValueError: If the info is not in the expected format.
445 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700446 kwargs.setdefault("service_account_email", info["client_email"])
447 kwargs.setdefault("token_uri", info["token_uri"])
Christophe Tatonb649b432018-02-08 14:12:23 -0800448 return cls(signer, **kwargs)
449
450 @classmethod
451 def from_service_account_info(cls, info, **kwargs):
452 """Creates a credentials instance from parsed service account info.
453
454 Args:
455 info (Mapping[str, str]): The service account info in Google
456 format.
457 kwargs: Additional arguments to pass to the constructor.
458
459 Returns:
460 google.auth.service_account.IDTokenCredentials: The constructed
461 credentials.
462
463 Raises:
464 ValueError: If the info is not in the expected format.
465 """
466 signer = _service_account_info.from_dict(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700467 info, require=["client_email", "token_uri"]
468 )
Christophe Tatonb649b432018-02-08 14:12:23 -0800469 return cls._from_signer_and_info(signer, info, **kwargs)
470
471 @classmethod
472 def from_service_account_file(cls, filename, **kwargs):
473 """Creates a credentials instance from a service account json file.
474
475 Args:
476 filename (str): The path to the service account json file.
477 kwargs: Additional arguments to pass to the constructor.
478
479 Returns:
480 google.auth.service_account.IDTokenCredentials: The constructed
481 credentials.
482 """
483 info, signer = _service_account_info.from_filename(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700484 filename, require=["client_email", "token_uri"]
485 )
Christophe Tatonb649b432018-02-08 14:12:23 -0800486 return cls._from_signer_and_info(signer, info, **kwargs)
487
488 def with_target_audience(self, target_audience):
489 """Create a copy of these credentials with the specified target
490 audience.
491
492 Args:
493 target_audience (str): The intended audience for these credentials,
494 used when requesting the ID Token.
495
496 Returns:
497 google.auth.service_account.IDTokenCredentials: A new credentials
498 instance.
499 """
500 return self.__class__(
501 self._signer,
502 service_account_email=self._service_account_email,
503 token_uri=self._token_uri,
504 target_audience=target_audience,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700505 additional_claims=self._additional_claims.copy(),
506 )
Christophe Tatonb649b432018-02-08 14:12:23 -0800507
508 def _make_authorization_grant_assertion(self):
509 """Create the OAuth 2.0 assertion.
510
511 This assertion is used during the OAuth 2.0 grant to acquire an
512 ID token.
513
514 Returns:
515 bytes: The authorization grant assertion.
516 """
517 now = _helpers.utcnow()
518 lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
519 expiry = now + lifetime
520
521 payload = {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700522 "iat": _helpers.datetime_to_secs(now),
523 "exp": _helpers.datetime_to_secs(expiry),
Christophe Tatonb649b432018-02-08 14:12:23 -0800524 # The issuer must be the service account email.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700525 "iss": self.service_account_email,
Christophe Tatonb649b432018-02-08 14:12:23 -0800526 # The audience must be the auth token endpoint's URI
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700527 "aud": self._token_uri,
Christophe Tatonb649b432018-02-08 14:12:23 -0800528 # The target audience specifies which service the ID token is
529 # intended for.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700530 "target_audience": self._target_audience,
Christophe Tatonb649b432018-02-08 14:12:23 -0800531 }
532
533 payload.update(self._additional_claims)
534
535 token = jwt.encode(self._signer, payload)
536
537 return token
538
539 @_helpers.copy_docstring(credentials.Credentials)
540 def refresh(self, request):
541 assertion = self._make_authorization_grant_assertion()
542 access_token, expiry, _ = _client.id_token_jwt_grant(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700543 request, self._token_uri, assertion
544 )
Christophe Tatonb649b432018-02-08 14:12:23 -0800545 self.token = access_token
546 self.expiry = expiry
547
548 @property
549 def service_account_email(self):
550 """The service account email."""
551 return self._service_account_email
552
553 @_helpers.copy_docstring(credentials.Signing)
554 def sign_bytes(self, message):
555 return self._signer.sign(message)
556
557 @property
558 @_helpers.copy_docstring(credentials.Signing)
559 def signer(self):
560 return self._signer
561
562 @property
563 @_helpers.copy_docstring(credentials.Signing)
564 def signer_email(self):
565 return self._service_account_email