2323from oauth2client import GOOGLE_TOKEN_URI
2424from oauth2client ._helpers import _json_encode
2525from oauth2client ._helpers import _from_bytes
26- from oauth2client ._helpers import _to_bytes
2726from oauth2client ._helpers import _urlsafe_b64encode
2827from oauth2client import util
2928from oauth2client .client import AssertionCredentials
3029from oauth2client .client import EXPIRY_FORMAT
30+ from oauth2client .client import SERVICE_ACCOUNT
3131from oauth2client import crypt
3232
3333
34- class _ServiceAccountCredentials (AssertionCredentials ):
35- """Class representing a service account ( signed JWT) credential."""
34+ class ServiceAccountCredentials (AssertionCredentials ):
35+ """Service Account credential for OAuth 2.0 signed JWT grants.
3636
37- MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
37+ Supports
38+
39+ * JSON keyfile (typically contains a PKCS8 key stored as
40+ PEM text)
41+
42+ Makes an assertion to server using a signed JWT assertion in exchange
43+ for an access token.
44+
45+ This credential does not require a flow to instantiate because it
46+ represents a two legged flow, and therefore has all of the required
47+ information to generate and refresh its own access tokens.
48+
49+ Args:
50+ service_account_email: string, The email associated with the
51+ service account.
52+ signer: ``crypt.Signer``, A signer which can be used to sign content.
53+ scopes: List or string, (Optional) Scopes to use when acquiring
54+ an access token.
55+ private_key_id: string, (Optional) Private key identifier. Typically
56+ only used with a JSON keyfile. Can be sent in the
57+ header of a JWT token assertion.
58+ client_id: string, (Optional) Client ID for the project that owns the
59+ service account.
60+ user_agent: string, (Optional) User agent to use when sending
61+ request.
62+ kwargs: dict, Extra key-value pairs (both strings) to send in the
63+ payload body when making an assertion.
64+ """
65+
66+ MAX_TOKEN_LIFETIME_SECS = 3600
67+ """Max lifetime of the token (one hour, in seconds)."""
3868
3969 NON_SERIALIZED_MEMBERS = (
4070 frozenset (['_signer' ]) |
4171 AssertionCredentials .NON_SERIALIZED_MEMBERS )
72+ """Members that aren't serialized when object is converted to JSON."""
73+
74+ # Can be over-ridden by factory constructors. Used for
75+ # serialization/deserialization purposes.
76+ _private_key_pkcs8_pem = None
4277
43- def __init__ (self , service_account_id , service_account_email ,
44- private_key_id , private_key_pkcs8_text , scopes ,
45- user_agent = None , token_uri = GOOGLE_TOKEN_URI ,
46- revoke_uri = GOOGLE_REVOKE_URI , ** kwargs ):
78+ def __init__ (self ,
79+ service_account_email ,
80+ signer ,
81+ scopes = '' ,
82+ private_key_id = None ,
83+ client_id = None ,
84+ user_agent = None ,
85+ ** kwargs ):
4786
48- super (_ServiceAccountCredentials , self ).__init__ (
49- None , user_agent = user_agent , token_uri = token_uri ,
50- revoke_uri = revoke_uri )
87+ super (ServiceAccountCredentials , self ).__init__ (
88+ None , user_agent = user_agent )
5189
52- self ._service_account_id = service_account_id
5390 self ._service_account_email = service_account_email
54- self ._private_key_id = private_key_id
55- self ._private_key_pkcs8_text = private_key_pkcs8_text
56- self ._signer = crypt .Signer .from_string (self ._private_key_pkcs8_text )
91+ self ._signer = signer
5792 self ._scopes = util .scopes_to_string (scopes )
93+ self ._private_key_id = private_key_id
94+ self .client_id = client_id
5895 self ._user_agent = user_agent
59- self ._token_uri = token_uri
60- self ._revoke_uri = revoke_uri
6196 self ._kwargs = kwargs
6297
98+ @classmethod
99+ def _from_parsed_json_keyfile (cls , keyfile_dict , scopes ):
100+ """Helper for factory constructors from JSON keyfile.
101+
102+ Args:
103+ keyfile_dict: dict-like object, The parsed dictionary-like object
104+ containing the contents of the JSON keyfile.
105+ scopes: List or string, Scopes to use when acquiring an
106+ access token.
107+
108+ Returns:
109+ ServiceAccountCredentials, a credentials object created from
110+ the keyfile contents.
111+
112+ Raises:
113+ ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
114+ KeyError, if one of the expected keys is not present in
115+ the keyfile.
116+ """
117+ creds_type = keyfile_dict .get ('type' )
118+ if creds_type != SERVICE_ACCOUNT :
119+ raise ValueError ('Unexpected credentials type' , creds_type ,
120+ 'Expected' , SERVICE_ACCOUNT )
121+
122+ service_account_email = keyfile_dict ['client_email' ]
123+ private_key_pkcs8_pem = keyfile_dict ['private_key' ]
124+ private_key_id = keyfile_dict ['private_key_id' ]
125+ client_id = keyfile_dict ['client_id' ]
126+
127+ signer = crypt .Signer .from_string (private_key_pkcs8_pem )
128+ credentials = cls (service_account_email , signer , scopes = scopes ,
129+ private_key_id = private_key_id ,
130+ client_id = client_id )
131+ credentials ._private_key_pkcs8_pem = private_key_pkcs8_pem
132+ return credentials
133+
134+ @classmethod
135+ def from_json_keyfile_name (cls , filename , scopes = '' ):
136+ """Factory constructor from JSON keyfile by name.
137+
138+ Args:
139+ filename: string, The location of the keyfile.
140+ scopes: List or string, (Optional) Scopes to use when acquiring an
141+ access token.
142+
143+ Returns:
144+ ServiceAccountCredentials, a credentials object created from
145+ the keyfile.
146+
147+ Raises:
148+ ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
149+ KeyError, if one of the expected keys is not present in
150+ the keyfile.
151+ """
152+ with open (filename , 'r' ) as file_obj :
153+ client_credentials = json .load (file_obj )
154+ return cls ._from_parsed_json_keyfile (client_credentials , scopes )
155+
156+ @classmethod
157+ def from_json_keyfile_dict (cls , keyfile_dict , scopes = '' ):
158+ """Factory constructor from parsed JSON keyfile.
159+
160+ Args:
161+ keyfile_dict: dict-like object, The parsed dictionary-like object
162+ containing the contents of the JSON keyfile.
163+ scopes: List or string, (Optional) Scopes to use when acquiring an
164+ access token.
165+
166+ Returns:
167+ ServiceAccountCredentials, a credentials object created from
168+ the keyfile.
169+
170+ Raises:
171+ ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
172+ KeyError, if one of the expected keys is not present in
173+ the keyfile.
174+ """
175+ return cls ._from_parsed_json_keyfile (keyfile_dict , scopes )
176+
63177 def _generate_assertion (self ):
64178 """Generate the assertion that will be used in the request."""
65179 now = int (time .time ())
66180 payload = {
67- 'aud' : self ._token_uri ,
181+ 'aud' : self .token_uri ,
68182 'scope' : self ._scopes ,
69183 'iat' : now ,
70184 'exp' : now + self .MAX_TOKEN_LIFETIME_SECS ,
@@ -85,26 +199,45 @@ def service_account_email(self):
85199 def serialization_data (self ):
86200 return {
87201 'type' : 'service_account' ,
88- 'client_id' : self ._service_account_id ,
89202 'client_email' : self ._service_account_email ,
90203 'private_key_id' : self ._private_key_id ,
91- 'private_key' : self ._private_key_pkcs8_text
204+ 'private_key' : self ._private_key_pkcs8_pem ,
205+ 'client_id' : self .client_id ,
92206 }
93207
94208 @classmethod
95- def from_json (cls , s ):
96- data = json .loads (_from_bytes (s ))
209+ def from_json (cls , json_data ):
210+ """Deserialize a JSON-serialized instance.
211+
212+ Inverse to :meth:`to_json`.
213+
214+ Args:
215+ json_data: dict or string, Serialized JSON (as a string or an
216+ already parsed dictionary) representing a credential.
217+
218+ Returns:
219+ ServiceAccountCredentials from the serialized data.
220+ """
221+ if not isinstance (json_data , dict ):
222+ json_data = json .loads (_from_bytes (json_data ))
97223
224+ private_key_pkcs8_pem = json_data ['_private_key_pkcs8_pem' ]
225+ signer = crypt .Signer .from_string (private_key_pkcs8_pem )
98226 credentials = cls (
99- service_account_id = data ['_service_account_id' ],
100- service_account_email = data ['_service_account_email' ],
101- private_key_id = data ['_private_key_id' ],
102- private_key_pkcs8_text = data ['_private_key_pkcs8_text' ],
103- scopes = [],
104- user_agent = data ['_user_agent' ])
105- credentials .invalid = data ['invalid' ]
106- credentials .access_token = data ['access_token' ]
107- token_expiry = data .get ('token_expiry' , None )
227+ json_data ['_service_account_email' ],
228+ signer ,
229+ scopes = json_data ['_scopes' ],
230+ private_key_id = json_data ['_private_key_id' ],
231+ client_id = json_data ['client_id' ],
232+ user_agent = json_data ['_user_agent' ],
233+ ** json_data ['_kwargs' ]
234+ )
235+ credentials ._private_key_pkcs8_pem = private_key_pkcs8_pem
236+ credentials .invalid = json_data ['invalid' ]
237+ credentials .access_token = json_data ['access_token' ]
238+ credentials .token_uri = json_data ['token_uri' ]
239+ credentials .revoke_uri = json_data ['revoke_uri' ]
240+ token_expiry = json_data .get ('token_expiry' , None )
108241 if token_expiry is not None :
109242 credentials .token_expiry = datetime .datetime .strptime (
110243 token_expiry , EXPIRY_FORMAT )
@@ -114,12 +247,13 @@ def create_scoped_required(self):
114247 return not self ._scopes
115248
116249 def create_scoped (self , scopes ):
117- return _ServiceAccountCredentials (self ._service_account_id ,
118- self ._service_account_email ,
119- self ._private_key_id ,
120- self ._private_key_pkcs8_text ,
121- scopes ,
122- user_agent = self ._user_agent ,
123- token_uri = self ._token_uri ,
124- revoke_uri = self ._revoke_uri ,
125- ** self ._kwargs )
250+ result = self .__class__ (self ._service_account_email ,
251+ self ._signer ,
252+ scopes = scopes ,
253+ private_key_id = self ._private_key_id ,
254+ client_id = self .client_id ,
255+ user_agent = self ._user_agent ,
256+ ** self ._kwargs )
257+ result .token_uri = self .token_uri
258+ result .revoke_uri = self .revoke_uri
259+ return result
0 commit comments