• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 Google Inc. All rights reserved.
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"""oauth2client Service account credentials class."""
16
17import base64
18import copy
19import datetime
20import json
21import time
22
23import oauth2client
24from oauth2client import _helpers
25from oauth2client import client
26from oauth2client import crypt
27from oauth2client import transport
28from oauth2client import util
29
30
31_PASSWORD_DEFAULT = 'notasecret'
32_PKCS12_KEY = '_private_key_pkcs12'
33_PKCS12_ERROR = r"""
34This library only implements PKCS#12 support via the pyOpenSSL library.
35Either install pyOpenSSL, or please convert the .p12 file
36to .pem format:
37    $ cat key.p12 | \
38    >   openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
39    >   openssl rsa > key.pem
40"""
41
42
43class ServiceAccountCredentials(client.AssertionCredentials):
44    """Service Account credential for OAuth 2.0 signed JWT grants.
45
46    Supports
47
48    * JSON keyfile (typically contains a PKCS8 key stored as
49      PEM text)
50    * ``.p12`` key (stores PKCS12 key and certificate)
51
52    Makes an assertion to server using a signed JWT assertion in exchange
53    for an access token.
54
55    This credential does not require a flow to instantiate because it
56    represents a two legged flow, and therefore has all of the required
57    information to generate and refresh its own access tokens.
58
59    Args:
60        service_account_email: string, The email associated with the
61                               service account.
62        signer: ``crypt.Signer``, A signer which can be used to sign content.
63        scopes: List or string, (Optional) Scopes to use when acquiring
64                an access token.
65        private_key_id: string, (Optional) Private key identifier. Typically
66                        only used with a JSON keyfile. Can be sent in the
67                        header of a JWT token assertion.
68        client_id: string, (Optional) Client ID for the project that owns the
69                   service account.
70        user_agent: string, (Optional) User agent to use when sending
71                    request.
72        token_uri: string, URI for token endpoint. For convenience defaults
73                   to Google's endpoints but any OAuth 2.0 provider can be
74                   used.
75        revoke_uri: string, URI for revoke endpoint.  For convenience defaults
76                   to Google's endpoints but any OAuth 2.0 provider can be
77                   used.
78        kwargs: dict, Extra key-value pairs (both strings) to send in the
79                payload body when making an assertion.
80    """
81
82    MAX_TOKEN_LIFETIME_SECS = 3600
83    """Max lifetime of the token (one hour, in seconds)."""
84
85    NON_SERIALIZED_MEMBERS = (
86        frozenset(['_signer']) |
87        client.AssertionCredentials.NON_SERIALIZED_MEMBERS)
88    """Members that aren't serialized when object is converted to JSON."""
89
90    # Can be over-ridden by factory constructors. Used for
91    # serialization/deserialization purposes.
92    _private_key_pkcs8_pem = None
93    _private_key_pkcs12 = None
94    _private_key_password = None
95
96    def __init__(self,
97                 service_account_email,
98                 signer,
99                 scopes='',
100                 private_key_id=None,
101                 client_id=None,
102                 user_agent=None,
103                 token_uri=oauth2client.GOOGLE_TOKEN_URI,
104                 revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
105                 **kwargs):
106
107        super(ServiceAccountCredentials, self).__init__(
108            None, user_agent=user_agent, token_uri=token_uri,
109            revoke_uri=revoke_uri)
110
111        self._service_account_email = service_account_email
112        self._signer = signer
113        self._scopes = util.scopes_to_string(scopes)
114        self._private_key_id = private_key_id
115        self.client_id = client_id
116        self._user_agent = user_agent
117        self._kwargs = kwargs
118
119    def _to_json(self, strip, to_serialize=None):
120        """Utility function that creates JSON repr. of a credentials object.
121
122        Over-ride is needed since PKCS#12 keys will not in general be JSON
123        serializable.
124
125        Args:
126            strip: array, An array of names of members to exclude from the
127                   JSON.
128            to_serialize: dict, (Optional) The properties for this object
129                          that will be serialized. This allows callers to
130                          modify before serializing.
131
132        Returns:
133            string, a JSON representation of this instance, suitable to pass to
134            from_json().
135        """
136        if to_serialize is None:
137            to_serialize = copy.copy(self.__dict__)
138        pkcs12_val = to_serialize.get(_PKCS12_KEY)
139        if pkcs12_val is not None:
140            to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val)
141        return super(ServiceAccountCredentials, self)._to_json(
142            strip, to_serialize=to_serialize)
143
144    @classmethod
145    def _from_parsed_json_keyfile(cls, keyfile_dict, scopes,
146                                  token_uri=None, revoke_uri=None):
147        """Helper for factory constructors from JSON keyfile.
148
149        Args:
150            keyfile_dict: dict-like object, The parsed dictionary-like object
151                          containing the contents of the JSON keyfile.
152            scopes: List or string, Scopes to use when acquiring an
153                    access token.
154            token_uri: string, URI for OAuth 2.0 provider token endpoint.
155                       If unset and not present in keyfile_dict, defaults
156                       to Google's endpoints.
157            revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
158                       If unset and not present in keyfile_dict, defaults
159                       to Google's endpoints.
160
161        Returns:
162            ServiceAccountCredentials, a credentials object created from
163            the keyfile contents.
164
165        Raises:
166            ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
167            KeyError, if one of the expected keys is not present in
168                the keyfile.
169        """
170        creds_type = keyfile_dict.get('type')
171        if creds_type != client.SERVICE_ACCOUNT:
172            raise ValueError('Unexpected credentials type', creds_type,
173                             'Expected', client.SERVICE_ACCOUNT)
174
175        service_account_email = keyfile_dict['client_email']
176        private_key_pkcs8_pem = keyfile_dict['private_key']
177        private_key_id = keyfile_dict['private_key_id']
178        client_id = keyfile_dict['client_id']
179        if not token_uri:
180            token_uri = keyfile_dict.get('token_uri',
181                                         oauth2client.GOOGLE_TOKEN_URI)
182        if not revoke_uri:
183            revoke_uri = keyfile_dict.get('revoke_uri',
184                                          oauth2client.GOOGLE_REVOKE_URI)
185
186        signer = crypt.Signer.from_string(private_key_pkcs8_pem)
187        credentials = cls(service_account_email, signer, scopes=scopes,
188                          private_key_id=private_key_id,
189                          client_id=client_id, token_uri=token_uri,
190                          revoke_uri=revoke_uri)
191        credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
192        return credentials
193
194    @classmethod
195    def from_json_keyfile_name(cls, filename, scopes='',
196                               token_uri=None, revoke_uri=None):
197
198        """Factory constructor from JSON keyfile by name.
199
200        Args:
201            filename: string, The location of the keyfile.
202            scopes: List or string, (Optional) Scopes to use when acquiring an
203                    access token.
204            token_uri: string, URI for OAuth 2.0 provider token endpoint.
205                       If unset and not present in the key file, defaults
206                       to Google's endpoints.
207            revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
208                       If unset and not present in the key file, defaults
209                       to Google's endpoints.
210
211        Returns:
212            ServiceAccountCredentials, a credentials object created from
213            the keyfile.
214
215        Raises:
216            ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
217            KeyError, if one of the expected keys is not present in
218                the keyfile.
219        """
220        with open(filename, 'r') as file_obj:
221            client_credentials = json.load(file_obj)
222        return cls._from_parsed_json_keyfile(client_credentials, scopes,
223                                             token_uri=token_uri,
224                                             revoke_uri=revoke_uri)
225
226    @classmethod
227    def from_json_keyfile_dict(cls, keyfile_dict, scopes='',
228                               token_uri=None, revoke_uri=None):
229        """Factory constructor from parsed JSON keyfile.
230
231        Args:
232            keyfile_dict: dict-like object, The parsed dictionary-like object
233                          containing the contents of the JSON keyfile.
234            scopes: List or string, (Optional) Scopes to use when acquiring an
235                    access token.
236            token_uri: string, URI for OAuth 2.0 provider token endpoint.
237                       If unset and not present in keyfile_dict, defaults
238                       to Google's endpoints.
239            revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
240                       If unset and not present in keyfile_dict, defaults
241                       to Google's endpoints.
242
243        Returns:
244            ServiceAccountCredentials, a credentials object created from
245            the keyfile.
246
247        Raises:
248            ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
249            KeyError, if one of the expected keys is not present in
250                the keyfile.
251        """
252        return cls._from_parsed_json_keyfile(keyfile_dict, scopes,
253                                             token_uri=token_uri,
254                                             revoke_uri=revoke_uri)
255
256    @classmethod
257    def _from_p12_keyfile_contents(cls, service_account_email,
258                                   private_key_pkcs12,
259                                   private_key_password=None, scopes='',
260                                   token_uri=oauth2client.GOOGLE_TOKEN_URI,
261                                   revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
262        """Factory constructor from JSON keyfile.
263
264        Args:
265            service_account_email: string, The email associated with the
266                                   service account.
267            private_key_pkcs12: string, The contents of a PKCS#12 keyfile.
268            private_key_password: string, (Optional) Password for PKCS#12
269                                  private key. Defaults to ``notasecret``.
270            scopes: List or string, (Optional) Scopes to use when acquiring an
271                    access token.
272            token_uri: string, URI for token endpoint. For convenience defaults
273                       to Google's endpoints but any OAuth 2.0 provider can be
274                       used.
275            revoke_uri: string, URI for revoke endpoint. For convenience
276                        defaults to Google's endpoints but any OAuth 2.0
277                        provider can be used.
278
279        Returns:
280            ServiceAccountCredentials, a credentials object created from
281            the keyfile.
282
283        Raises:
284            NotImplementedError if pyOpenSSL is not installed / not the
285            active crypto library.
286        """
287        if private_key_password is None:
288            private_key_password = _PASSWORD_DEFAULT
289        if crypt.Signer is not crypt.OpenSSLSigner:
290            raise NotImplementedError(_PKCS12_ERROR)
291        signer = crypt.Signer.from_string(private_key_pkcs12,
292                                          private_key_password)
293        credentials = cls(service_account_email, signer, scopes=scopes,
294                          token_uri=token_uri, revoke_uri=revoke_uri)
295        credentials._private_key_pkcs12 = private_key_pkcs12
296        credentials._private_key_password = private_key_password
297        return credentials
298
299    @classmethod
300    def from_p12_keyfile(cls, service_account_email, filename,
301                         private_key_password=None, scopes='',
302                         token_uri=oauth2client.GOOGLE_TOKEN_URI,
303                         revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
304
305        """Factory constructor from JSON keyfile.
306
307        Args:
308            service_account_email: string, The email associated with the
309                                   service account.
310            filename: string, The location of the PKCS#12 keyfile.
311            private_key_password: string, (Optional) Password for PKCS#12
312                                  private key. Defaults to ``notasecret``.
313            scopes: List or string, (Optional) Scopes to use when acquiring an
314                    access token.
315            token_uri: string, URI for token endpoint. For convenience defaults
316                       to Google's endpoints but any OAuth 2.0 provider can be
317                       used.
318            revoke_uri: string, URI for revoke endpoint. For convenience
319                        defaults to Google's endpoints but any OAuth 2.0
320                        provider can be used.
321
322        Returns:
323            ServiceAccountCredentials, a credentials object created from
324            the keyfile.
325
326        Raises:
327            NotImplementedError if pyOpenSSL is not installed / not the
328            active crypto library.
329        """
330        with open(filename, 'rb') as file_obj:
331            private_key_pkcs12 = file_obj.read()
332        return cls._from_p12_keyfile_contents(
333            service_account_email, private_key_pkcs12,
334            private_key_password=private_key_password, scopes=scopes,
335            token_uri=token_uri, revoke_uri=revoke_uri)
336
337    @classmethod
338    def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
339                                private_key_password=None, scopes='',
340                                token_uri=oauth2client.GOOGLE_TOKEN_URI,
341                                revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
342        """Factory constructor from JSON keyfile.
343
344        Args:
345            service_account_email: string, The email associated with the
346                                   service account.
347            file_buffer: stream, A buffer that implements ``read()``
348                         and contains the PKCS#12 key contents.
349            private_key_password: string, (Optional) Password for PKCS#12
350                                  private key. Defaults to ``notasecret``.
351            scopes: List or string, (Optional) Scopes to use when acquiring an
352                    access token.
353            token_uri: string, URI for token endpoint. For convenience defaults
354                       to Google's endpoints but any OAuth 2.0 provider can be
355                       used.
356            revoke_uri: string, URI for revoke endpoint. For convenience
357                        defaults to Google's endpoints but any OAuth 2.0
358                        provider can be used.
359
360        Returns:
361            ServiceAccountCredentials, a credentials object created from
362            the keyfile.
363
364        Raises:
365            NotImplementedError if pyOpenSSL is not installed / not the
366            active crypto library.
367        """
368        private_key_pkcs12 = file_buffer.read()
369        return cls._from_p12_keyfile_contents(
370            service_account_email, private_key_pkcs12,
371            private_key_password=private_key_password, scopes=scopes,
372            token_uri=token_uri, revoke_uri=revoke_uri)
373
374    def _generate_assertion(self):
375        """Generate the assertion that will be used in the request."""
376        now = int(time.time())
377        payload = {
378            'aud': self.token_uri,
379            'scope': self._scopes,
380            'iat': now,
381            'exp': now + self.MAX_TOKEN_LIFETIME_SECS,
382            'iss': self._service_account_email,
383        }
384        payload.update(self._kwargs)
385        return crypt.make_signed_jwt(self._signer, payload,
386                                     key_id=self._private_key_id)
387
388    def sign_blob(self, blob):
389        """Cryptographically sign a blob (of bytes).
390
391        Implements abstract method
392        :meth:`oauth2client.client.AssertionCredentials.sign_blob`.
393
394        Args:
395            blob: bytes, Message to be signed.
396
397        Returns:
398            tuple, A pair of the private key ID used to sign the blob and
399            the signed contents.
400        """
401        return self._private_key_id, self._signer.sign(blob)
402
403    @property
404    def service_account_email(self):
405        """Get the email for the current service account.
406
407        Returns:
408            string, The email associated with the service account.
409        """
410        return self._service_account_email
411
412    @property
413    def serialization_data(self):
414        # NOTE: This is only useful for JSON keyfile.
415        return {
416            'type': 'service_account',
417            'client_email': self._service_account_email,
418            'private_key_id': self._private_key_id,
419            'private_key': self._private_key_pkcs8_pem,
420            'client_id': self.client_id,
421        }
422
423    @classmethod
424    def from_json(cls, json_data):
425        """Deserialize a JSON-serialized instance.
426
427        Inverse to :meth:`to_json`.
428
429        Args:
430            json_data: dict or string, Serialized JSON (as a string or an
431                       already parsed dictionary) representing a credential.
432
433        Returns:
434            ServiceAccountCredentials from the serialized data.
435        """
436        if not isinstance(json_data, dict):
437            json_data = json.loads(_helpers._from_bytes(json_data))
438
439        private_key_pkcs8_pem = None
440        pkcs12_val = json_data.get(_PKCS12_KEY)
441        password = None
442        if pkcs12_val is None:
443            private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
444            signer = crypt.Signer.from_string(private_key_pkcs8_pem)
445        else:
446            # NOTE: This assumes that private_key_pkcs8_pem is not also
447            #       in the serialized data. This would be very incorrect
448            #       state.
449            pkcs12_val = base64.b64decode(pkcs12_val)
450            password = json_data['_private_key_password']
451            signer = crypt.Signer.from_string(pkcs12_val, password)
452
453        credentials = cls(
454            json_data['_service_account_email'],
455            signer,
456            scopes=json_data['_scopes'],
457            private_key_id=json_data['_private_key_id'],
458            client_id=json_data['client_id'],
459            user_agent=json_data['_user_agent'],
460            **json_data['_kwargs']
461        )
462        if private_key_pkcs8_pem is not None:
463            credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
464        if pkcs12_val is not None:
465            credentials._private_key_pkcs12 = pkcs12_val
466        if password is not None:
467            credentials._private_key_password = password
468        credentials.invalid = json_data['invalid']
469        credentials.access_token = json_data['access_token']
470        credentials.token_uri = json_data['token_uri']
471        credentials.revoke_uri = json_data['revoke_uri']
472        token_expiry = json_data.get('token_expiry', None)
473        if token_expiry is not None:
474            credentials.token_expiry = datetime.datetime.strptime(
475                token_expiry, client.EXPIRY_FORMAT)
476        return credentials
477
478    def create_scoped_required(self):
479        return not self._scopes
480
481    def create_scoped(self, scopes):
482        result = self.__class__(self._service_account_email,
483                                self._signer,
484                                scopes=scopes,
485                                private_key_id=self._private_key_id,
486                                client_id=self.client_id,
487                                user_agent=self._user_agent,
488                                **self._kwargs)
489        result.token_uri = self.token_uri
490        result.revoke_uri = self.revoke_uri
491        result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
492        result._private_key_pkcs12 = self._private_key_pkcs12
493        result._private_key_password = self._private_key_password
494        return result
495
496    def create_with_claims(self, claims):
497        """Create credentials that specify additional claims.
498
499        Args:
500            claims: dict, key-value pairs for claims.
501
502        Returns:
503            ServiceAccountCredentials, a copy of the current service account
504            credentials with updated claims to use when obtaining access
505            tokens.
506        """
507        new_kwargs = dict(self._kwargs)
508        new_kwargs.update(claims)
509        result = self.__class__(self._service_account_email,
510                                self._signer,
511                                scopes=self._scopes,
512                                private_key_id=self._private_key_id,
513                                client_id=self.client_id,
514                                user_agent=self._user_agent,
515                                **new_kwargs)
516        result.token_uri = self.token_uri
517        result.revoke_uri = self.revoke_uri
518        result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
519        result._private_key_pkcs12 = self._private_key_pkcs12
520        result._private_key_password = self._private_key_password
521        return result
522
523    def create_delegated(self, sub):
524        """Create credentials that act as domain-wide delegation of authority.
525
526        Use the ``sub`` parameter as the subject to delegate on behalf of
527        that user.
528
529        For example::
530
531          >>> account_sub = 'foo@email.com'
532          >>> delegate_creds = creds.create_delegated(account_sub)
533
534        Args:
535            sub: string, An email address that this service account will
536                 act on behalf of (via domain-wide delegation).
537
538        Returns:
539            ServiceAccountCredentials, a copy of the current service account
540            updated to act on behalf of ``sub``.
541        """
542        return self.create_with_claims({'sub': sub})
543
544
545def _datetime_to_secs(utc_time):
546    # TODO(issue 298): use time_delta.total_seconds()
547    # time_delta.total_seconds() not supported in Python 2.6
548    epoch = datetime.datetime(1970, 1, 1)
549    time_delta = utc_time - epoch
550    return time_delta.days * 86400 + time_delta.seconds
551
552
553class _JWTAccessCredentials(ServiceAccountCredentials):
554    """Self signed JWT credentials.
555
556    Makes an assertion to server using a self signed JWT from service account
557    credentials.  These credentials do NOT use OAuth 2.0 and instead
558    authenticate directly.
559    """
560    _MAX_TOKEN_LIFETIME_SECS = 3600
561    """Max lifetime of the token (one hour, in seconds)."""
562
563    def __init__(self,
564                 service_account_email,
565                 signer,
566                 scopes=None,
567                 private_key_id=None,
568                 client_id=None,
569                 user_agent=None,
570                 token_uri=oauth2client.GOOGLE_TOKEN_URI,
571                 revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
572                 additional_claims=None):
573        if additional_claims is None:
574            additional_claims = {}
575        super(_JWTAccessCredentials, self).__init__(
576            service_account_email,
577            signer,
578            private_key_id=private_key_id,
579            client_id=client_id,
580            user_agent=user_agent,
581            token_uri=token_uri,
582            revoke_uri=revoke_uri,
583            **additional_claims)
584
585    def authorize(self, http):
586        """Authorize an httplib2.Http instance with a JWT assertion.
587
588        Unless specified, the 'aud' of the assertion will be the base
589        uri of the request.
590
591        Args:
592            http: An instance of ``httplib2.Http`` or something that acts
593                  like it.
594        Returns:
595            A modified instance of http that was passed in.
596        Example::
597            h = httplib2.Http()
598            h = credentials.authorize(h)
599        """
600        transport.wrap_http_for_jwt_access(self, http)
601        return http
602
603    def get_access_token(self, http=None, additional_claims=None):
604        """Create a signed jwt.
605
606        Args:
607            http: unused
608            additional_claims: dict, additional claims to add to
609                the payload of the JWT.
610        Returns:
611            An AccessTokenInfo with the signed jwt
612        """
613        if additional_claims is None:
614            if self.access_token is None or self.access_token_expired:
615                self.refresh(None)
616            return client.AccessTokenInfo(
617              access_token=self.access_token, expires_in=self._expires_in())
618        else:
619            # Create a 1 time token
620            token, unused_expiry = self._create_token(additional_claims)
621            return client.AccessTokenInfo(
622              access_token=token, expires_in=self._MAX_TOKEN_LIFETIME_SECS)
623
624    def revoke(self, http):
625        """Cannot revoke JWTAccessCredentials tokens."""
626        pass
627
628    def create_scoped_required(self):
629        # JWTAccessCredentials are unscoped by definition
630        return True
631
632    def create_scoped(self, scopes, token_uri=oauth2client.GOOGLE_TOKEN_URI,
633                      revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
634        # Returns an OAuth2 credentials with the given scope
635        result = ServiceAccountCredentials(self._service_account_email,
636                                           self._signer,
637                                           scopes=scopes,
638                                           private_key_id=self._private_key_id,
639                                           client_id=self.client_id,
640                                           user_agent=self._user_agent,
641                                           token_uri=token_uri,
642                                           revoke_uri=revoke_uri,
643                                           **self._kwargs)
644        if self._private_key_pkcs8_pem is not None:
645            result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
646        if self._private_key_pkcs12 is not None:
647            result._private_key_pkcs12 = self._private_key_pkcs12
648        if self._private_key_password is not None:
649            result._private_key_password = self._private_key_password
650        return result
651
652    def refresh(self, http):
653        self._refresh(None)
654
655    def _refresh(self, http_request):
656        self.access_token, self.token_expiry = self._create_token()
657
658    def _create_token(self, additional_claims=None):
659        now = client._UTCNOW()
660        lifetime = datetime.timedelta(seconds=self._MAX_TOKEN_LIFETIME_SECS)
661        expiry = now + lifetime
662        payload = {
663            'iat': _datetime_to_secs(now),
664            'exp': _datetime_to_secs(expiry),
665            'iss': self._service_account_email,
666            'sub': self._service_account_email
667        }
668        payload.update(self._kwargs)
669        if additional_claims is not None:
670            payload.update(additional_claims)
671        jwt = crypt.make_signed_jwt(self._signer, payload,
672                                    key_id=self._private_key_id)
673        return jwt.decode('ascii'), expiry
674