Skip to content
This repository was archived by the owner on Jan 20, 2024. It is now read-only.

Commit 40c9379

Browse files
sfcbetiucmanivinesh
sfcbetiuc
authored andcommitted
Refresh Token, Web/Public App support (#116)
* Implemented RefreshToken, Public-Web app support for OAuth2.0 * Added unit tests and other small changes Added unit tests for create_payload method, added venv in .gitignore, block of code for setting redirectURI was misplaced * Added a unit test, fixed bugs Added the test_authToken_should_differ_if_refresh_token_is_enforced unit test, fixed two bugs in client.py * Updated the README.md file * Renamed a test method and added a class docstring for the test class * Addressed code review requested changes * Updated SDK version from 1.2.0 to 1.3.0 Updated SDK version from 1.2.0 to 1.3.0 in FuelSDK/__init__.py, client.py, rest.py and setup.py * Added tests config * Small changes in FuelSDK/Public_WebAppTests/test_ET_Client.py Replaced the setUp with setUpClass and updated the docstring
1 parent 34eb519 commit 40c9379

File tree

9 files changed

+236
-25
lines changed

9 files changed

+236
-25
lines changed

Diff for: .gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ objsamples/ET_Client.py
3131
objsamples/config.python
3232
objsamples/*.pyc
3333
config.python
34+
FuelSDK/Public_WebAppTests/config.python
3435

3536
soap_cache_file.json
36-
.idea/
37+
.idea/
38+
venv

Diff for: FuelSDK/Public_WebAppTests/__init__.py

Whitespace-only changes.

Diff for: FuelSDK/Public_WebAppTests/config.python.template

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[Web Services]
2+
appsignature: none
3+
clientid: <CLIENT_ID>
4+
clientsecret: <CLIENT_SECRET>
5+
defaultwsdl: https://webservice.exacttarget.com/etframework.wsdl
6+
authenticationurl: <AUTH TENANT SPECIFIC ENDPOINT>
7+
wsdl_file_local_loc: <WSDL_PATH>/ExactTargetWSDL.xml
8+
9+
[Auth Service]
10+
useOAuth2Authentication: True
11+
accountId: <TARGET_ACCOUNT_ID>
12+
scope: <PERMISSION_LIST>
13+
applicationType: <APPLICATION_TYPE>
14+
redirectURI: <REDIRECT_URI_FOR_PUBLIC/WEB_APP>
15+
authorizationCode: <AUTHORIZATION_CODE_FOR_PUBLIC/WEB_APP>

Diff for: FuelSDK/Public_WebAppTests/test_ET_Client.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from unittest import TestCase
2+
from FuelSDK import ET_Client
3+
4+
5+
class TestET_Client(TestCase):
6+
7+
@classmethod
8+
def setUpClass(cls):
9+
"""
10+
test_authToken_and_refreshKey_should_differ_if_refresh_token_is_enforced expects a Public/Web App config.
11+
All the other tests require a Server to Server config
12+
"""
13+
14+
cls.client = ET_Client(False, False)
15+
16+
def test_authToken_and_refreshKey_should_differ_if_refresh_token_is_enforced(self):
17+
self.authToken1 = self.client.authToken
18+
self.refreshKey1 = self.client.refreshKey
19+
20+
self.client.refresh_token_with_oAuth2(True)
21+
22+
self.authToken2 = self.client.authToken
23+
self.refreshKey2 = self.client.refreshKey
24+
25+
self.assertNotEqual(self.authToken1, self.authToken2)
26+
self.assertNotEqual(self.refreshKey1, self.refreshKey2)
27+
28+
def test_auth_payload_should_have_public_app_attributes(self):
29+
self.client.application_type = 'public'
30+
31+
payload = self.client.create_payload()
32+
33+
self.assertEqual(self.client.client_id, payload['client_id'])
34+
self.assertEqual(self.client.redirect_URI, payload['redirect_uri'])
35+
self.assertEqual(self.client.authorization_code, payload['code'])
36+
self.assertEqual('authorization_code', payload['grant_type'])
37+
38+
def test_auth_payload_for_public_app_should_not_have_client_secret(self):
39+
self.client.application_type = 'public'
40+
41+
payload = self.client.create_payload()
42+
43+
self.assertRaises(KeyError, lambda: payload['client_secret'])
44+
45+
def test_auth_payload_should_have_web_app_attributes(self):
46+
self.client.application_type = 'web'
47+
48+
payload = self.client.create_payload()
49+
50+
self.assertEqual('authorization_code', payload['grant_type'])
51+
self.assertEqual(self.client.client_id, payload['client_id'])
52+
self.assertEqual(self.client.client_secret, payload['client_secret'])
53+
self.assertEqual(self.client.redirect_URI, payload['redirect_uri'])
54+
self.assertEqual(self.client.authorization_code, payload['code'])
55+
56+
def test_auth_payload_should_have_server_app_attributes(self):
57+
self.client.application_type = 'server'
58+
59+
payload = self.client.create_payload()
60+
61+
self.assertEqual('client_credentials', payload['grant_type'])
62+
self.assertEqual(self.client.client_id, payload['client_id'])
63+
self.assertEqual(self.client.client_secret, payload['client_secret'])
64+
65+
def test_auth_payload_for_server_app_should_not_have_code_and_redirect_uri(self):
66+
self.client.application_type = 'server'
67+
68+
payload = self.client.create_payload()
69+
70+
self.assertRaises(KeyError, lambda: payload['code'])
71+
self.assertRaises(KeyError, lambda: payload['redirect_uri'])
72+
73+
def test_auth_payload_with_refresh_token_should_have_refresh_token_attribute(self):
74+
self.client.refreshKey = 'RefreshKey'
75+
76+
payload = self.client.create_payload()
77+
78+
self.assertEqual('refresh_token', payload['grant_type'])
79+
self.assertEqual(self.client.refreshKey, payload['refresh_token'])

Diff for: FuelSDK/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = '1.2.0'
1+
__version__ = '1.3.0'
22

33
# Runtime patch the suds library
44
from FuelSDK.suds_patch import _PropertyAppender

Diff for: FuelSDK/client.py

+79-17
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class ET_Client(object):
3737
use_oAuth2_authentication = None
3838
account_id = None
3939
scope = None
40+
application_type = None
41+
authorization_code = None
42+
redirect_URI = None
4043

4144
## get_server_wsdl - if True and a newer WSDL is on the server than the local filesystem retrieve it
4245
def __init__(self, get_server_wsdl = False, debug = False, params = None, tokenResponse=None):
@@ -50,8 +53,13 @@ def __init__(self, get_server_wsdl = False, debug = False, params = None, tokenR
5053
else:
5154
logging.getLogger('suds').setLevel(logging.INFO)
5255

56+
self.configure_client(get_server_wsdl, params, tokenResponse)
57+
58+
def configure_client(self, get_server_wsdl, params, tokenResponse):
59+
5360
## Read the config information out of config.python
5461
config = configparser.RawConfigParser()
62+
5563
if os.path.exists(os.path.expanduser('~/.fuelsdk/config.python')):
5664
config.read(os.path.expanduser('~/.fuelsdk/config.python'))
5765
else:
@@ -148,8 +156,45 @@ def __init__(self, get_server_wsdl = False, debug = False, params = None, tokenR
148156
elif "FUELSDK_SCOPE" in os.environ:
149157
self.scope = os.environ["FUELSDK_SCOPE"]
150158

159+
if params is not None and "authorizationCode" in params:
160+
self.authorization_code = params["authorizationCode"]
161+
elif config.has_option("Auth Service", "authorizationCode"):
162+
self.authorization_code = config.get("Auth Service", "authorizationCode")
163+
elif "FUELSDK_AUTHORIZATION_CODE" in os.environ:
164+
self.authorization_code = os.environ["FUELSDK_AUTHORIZATION_CODE"]
165+
166+
if params is not None and "applicationType" in params:
167+
self.application_type = params["applicationType"]
168+
elif config.has_option("Auth Service", "applicationType"):
169+
self.application_type = config.get("Auth Service", "applicationType")
170+
elif "FUELSDK_APPLICATION_TYPE" in os.environ:
171+
self.application_type = os.environ["FUELSDK_APPLICATION_TYPE"]
172+
173+
if self.is_none_or_empty_or_blank(self.application_type):
174+
self.application_type = "server"
175+
176+
if params is not None and "redirectURI" in params:
177+
self.redirect_URI = params["redirectURI"]
178+
elif config.has_option("Auth Service", "redirectURI"):
179+
self.redirect_URI = config.get("Auth Service", "redirectURI")
180+
elif "FUELSDK_REDIRECT_URI" in os.environ:
181+
self.redirect_URI = os.environ["FUELSDK_REDIRECT_URI"]
182+
183+
if self.application_type in ["public", "web"]:
184+
if self.is_none_or_empty_or_blank(self.authorization_code) or self.is_none_or_empty_or_blank(self.redirect_URI):
185+
raise Exception('authorizationCode or redirectURI is null: For Public/Web Apps, the authorizationCode and redirectURI must be '
186+
'passed when instantiating ET_Client or must be provided in environment variables/config file')
187+
188+
if self.application_type == "public":
189+
if self.is_none_or_empty_or_blank(self.client_id):
190+
raise Exception('clientid is null: clientid must be passed when instantiating ET_Client or must be provided in environment variables / config file')
191+
else: # application_type is server or web
192+
if self.is_none_or_empty_or_blank(self.client_id) or self.is_none_or_empty_or_blank(self.client_secret):
193+
raise Exception('clientid or clientsecret is null: clientid and clientsecret must be passed when instantiating ET_Client '
194+
'or must be provided in environment variables / config file')
195+
151196
## get the JWT from the params if passed in...or go to the server to get it
152-
if(params is not None and 'jwt' in params):
197+
if (params is not None and 'jwt' in params):
153198
decodedJWT = jwt.decode(params['jwt'], self.appsignature)
154199
self.authToken = decodedJWT['request']['user']['oauthToken']
155200
self.authTokenExpiration = time.time() + decodedJWT['request']['user']['expiresIn']
@@ -203,7 +248,7 @@ def build_soap_client(self):
203248

204249
self.soap_client = suds.client.Client(self.wsdl_file_url, faults=False, cachingpolicy=1)
205250
self.soap_client.set_options(location=self.soap_endpoint)
206-
self.soap_client.set_options(headers={'user-agent' : 'FuelSDK-Python-v1.2.0'})
251+
self.soap_client.set_options(headers={'user-agent' : 'FuelSDK-Python-v1.3.0'})
207252

208253
if self.use_oAuth2_authentication == 'True':
209254
element_oAuth = Element('fueloauth', ns=('etns', 'http://exacttarget.com'))
@@ -232,7 +277,7 @@ def refresh_token(self, force_refresh = False):
232277

233278
#If we don't already have a token or the token expires within 5 min(300 seconds), get one
234279
if (force_refresh or self.authToken is None or (self.authTokenExpiration is not None and time.time() + 300 > self.authTokenExpiration)):
235-
headers = {'content-type' : 'application/json', 'user-agent' : 'FuelSDK-Python-v1.2.0'}
280+
headers = {'content-type' : 'application/json', 'user-agent' : 'FuelSDK-Python-v1.3.0'}
236281
if (self.authToken is None):
237282
payload = {'clientId' : self.client_id, 'clientSecret' : self.client_secret, 'accessType': 'offline'}
238283
else:
@@ -268,21 +313,13 @@ def refresh_token_with_oAuth2(self, force_refresh=False):
268313
or self.authTokenExpiration is not None and time.time() + 300 > self.authTokenExpiration:
269314

270315
headers = {'content-type': 'application/json',
271-
'user-agent': 'FuelSDK-Python-v1.2.0'}
272-
273-
payload = {'client_id': self.client_id,
274-
'client_secret': self.client_secret,
275-
'grant_type': 'client_credentials'
276-
}
316+
'user-agent': 'FuelSDK-Python-v1.3.0'}
277317

278-
if self.account_id is not None and self.account_id.strip() != '':
279-
payload['account_id'] = self.account_id
280-
if self.scope is not None and self.scope.strip() != '':
281-
payload['scope'] = self.scope
318+
payload = self.create_payload()
282319

283-
self.auth_url = self.auth_url.strip() + '/v2/token'
320+
auth_endpoint = self.auth_url.strip() + '/v2/token'
284321

285-
r = requests.post(self.auth_url, data=json.dumps(payload), headers=headers)
322+
r = requests.post(auth_endpoint, data=json.dumps(payload), headers=headers)
286323
tokenResponse = r.json()
287324

288325
if 'access_token' not in tokenResponse:
@@ -294,8 +331,33 @@ def refresh_token_with_oAuth2(self, force_refresh=False):
294331
self.soap_endpoint = tokenResponse['soap_instance_url'] + 'service.asmx'
295332
self.base_api_url = tokenResponse['rest_instance_url']
296333

334+
if 'refresh_token' in tokenResponse:
335+
self.refreshKey = tokenResponse['refresh_token']
336+
297337
self.build_soap_client()
298338

339+
def create_payload(self):
340+
payload = {'client_id': self.client_id}
341+
342+
if self.application_type != "public":
343+
payload['client_secret'] = self.client_secret
344+
345+
if not self.is_none_or_empty_or_blank(self.refreshKey):
346+
payload['grant_type'] = "refresh_token"
347+
payload['refresh_token'] = self.refreshKey
348+
elif self.application_type in ["public", "web"]:
349+
payload['grant_type'] = "authorization_code"
350+
payload['code'] = self.authorization_code
351+
payload['redirect_uri'] = self.redirect_URI
352+
else:
353+
payload['grant_type'] = "client_credentials"
354+
355+
if not self.is_none_or_empty_or_blank(self.account_id):
356+
payload['account_id'] = self.account_id
357+
if not self.is_none_or_empty_or_blank(self.scope):
358+
payload['scope'] = self.scope
359+
360+
return payload
299361

300362
def get_soap_cache_file(self):
301363
json_data = {}
@@ -329,7 +391,7 @@ def get_soap_endpoint(self):
329391
"""
330392
try:
331393
r = requests.get(self.base_api_url + '/platform/v1/endpoints/soap', headers={
332-
'user-agent': 'FuelSDK-Python-v1.2.0',
394+
'user-agent': 'FuelSDK-Python-v1.3.0',
333395
'authorization': 'Bearer ' + self.authToken
334396
})
335397

@@ -387,4 +449,4 @@ def CreateDataExtensions(self, dataExtensionDefinitions):
387449
def is_none_or_empty_or_blank(self, str):
388450
if str and str.strip():
389451
return False
390-
return True
452+
return True

Diff for: FuelSDK/rest.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ def __init__(self, auth_stub, endpoint, qs = None):
331331
fullendpoint += urlSeparator + qStringValue + '=' + str(qs[qStringValue])
332332
urlSeparator = '&'
333333

334-
headers = {'authorization' : 'Bearer ' + auth_stub.authToken, 'user-agent' : 'FuelSDK-Python-v1.2.0'}
334+
headers = {'authorization' : 'Bearer ' + auth_stub.authToken, 'user-agent' : 'FuelSDK-Python-v1.3.0'}
335335
r = requests.get(fullendpoint, headers=headers)
336336

337337

@@ -349,7 +349,7 @@ class ET_PostRest(ET_Constructor):
349349
def __init__(self, auth_stub, endpoint, payload):
350350
auth_stub.refresh_token()
351351

352-
headers = {'content-type' : 'application/json', 'user-agent' : 'FuelSDK-Python-v1.2.0', 'authorization' : 'Bearer ' + auth_stub.authToken}
352+
headers = {'content-type' : 'application/json', 'user-agent' : 'FuelSDK-Python-v1.3.0', 'authorization' : 'Bearer ' + auth_stub.authToken}
353353
r = requests.post(endpoint, data=json.dumps(payload), headers=headers)
354354

355355
obj = super(ET_PostRest, self).__init__(r, True)
@@ -364,7 +364,7 @@ class ET_PatchRest(ET_Constructor):
364364
def __init__(self, auth_stub, endpoint, payload):
365365
auth_stub.refresh_token()
366366

367-
headers = {'content-type' : 'application/json', 'user-agent' : 'FuelSDK-Python-v1.2.0', 'authorization' : 'Bearer ' + auth_stub.authToken}
367+
headers = {'content-type' : 'application/json', 'user-agent' : 'FuelSDK-Python-v1.3.0', 'authorization' : 'Bearer ' + auth_stub.authToken}
368368
r = requests.patch(endpoint , data=json.dumps(payload), headers=headers)
369369

370370
obj = super(ET_PatchRest, self).__init__(r, True)
@@ -379,7 +379,7 @@ class ET_DeleteRest(ET_Constructor):
379379
def __init__(self, auth_stub, endpoint):
380380
auth_stub.refresh_token()
381381

382-
headers = {'authorization' : 'Bearer ' + auth_stub.authToken, 'user-agent' : 'FuelSDK-Python-v1.2.0'}
382+
headers = {'authorization' : 'Bearer ' + auth_stub.authToken, 'user-agent' : 'FuelSDK-Python-v1.3.0'}
383383
r = requests.delete(endpoint, headers=headers)
384384

385385
obj = super(ET_DeleteRest, self).__init__(r, True)

Diff for: README.md

+54-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,64 @@
1-
# FuelSDK-Python v1.2.0
1+
# FuelSDK-Python v1.3.0
22

33
Salesforce Marketing Cloud Fuel SDK for Python
44

55
## Overview
66

77
The Fuel SDK for Python provides easy access to Salesforce Marketing Cloud's Fuel API Family services, including a collection of REST APIs and a SOAP API. These APIs provide access to Salesforce Marketing Cloud functionality via common collection types such as array/hash.
88

9+
New Features in Version 1.3.0
10+
------------
11+
* Added Refresh Token support for OAuth2 authentication
12+
* Added Web/Public App support for OAuth2 authentication
13+
14+
More details on Access Tokens for Web/Public Apps can be found [here](https://developer.salesforce.com/docs/atlas.en-us.mc-app-development.meta/mc-app-development/access-token-app.htm)
15+
16+
Sample config:
17+
18+
```
19+
[Web Services]
20+
appsignature: none
21+
clientid: <CLIENT_ID>
22+
clientsecret: <CLIENT_SECRET>
23+
defaultwsdl: https://webservice.exacttarget.com/etframework.wsdl
24+
authenticationurl: <AUTH TENANT SPECIFIC ENDPOINT>
25+
baseapiurl: <REST TENANT SPECIFIC ENDPOINT>
26+
soapendpoint: <SOAP TENANT SPECIFIC ENDPOINT>
27+
wsdl_file_local_loc: <WSDL_PATH>/ExactTargetWSDL.xml
28+
29+
[Auth Service]
30+
useOAuth2Authentication: True
31+
accountId: <TARGET_ACCOUNT_ID>
32+
scope: <PERMISSION_LIST>
33+
applicationType: <APPLICATION_TYPE>
34+
redirectURI: <REDIRECT_URI_FOR_PUBLIC/WEB_APP>
35+
authorizationCode: <AUTHORIZATION_CODE_FOR_PUBLIC/WEB_APP>
36+
```
37+
38+
Example passing config as a parameter to ET_Client constructor:
39+
40+
```
41+
stubObj = ET_Client.ET_Client(
42+
False, False,
43+
{
44+
'clientid': '<CLIENT_ID>',
45+
'clientsecret': '<CLIENT_SECRET>',
46+
'defaultwsdl': 'https://webservice.exacttarget.com/etframework.wsdl',
47+
'authenticationurl': '<AUTH TENANT SPECIFIC ENDPOINT>',
48+
'baseapiurl': '<REST TENANT SPECIFIC ENDPOINT>',
49+
'soapendpoint': '<SOAP TENANT SPECIFIC ENDPOINT>',
50+
'wsdl_file_local_loc': r'<WSDL_PATH>/ExactTargetWSDL.xml',
51+
'useOAuth2Authentication': 'True',
52+
'accountId': '<TARGET_ACCOUNT_ID>',
53+
'scope': '<PERMISSION_LIST>'
54+
'applicationType': '<APPLICATION_TYPE>'
55+
'redirectURI': '<REDIRECT_URI_FOR_PUBLIC/WEB_APP>'
56+
'authorizationCode': '<AUTHORIZATION_CODE_FOR_PUBLIC/WEB_APP>'
57+
})
58+
```
59+
60+
* applicationType can have one of the following values: `server`, `public`, `web`. The default value of applicationType is `server`.
61+
962
New Features in Version 1.2.0
1063
------------
1164
* Added support for OAuth2 authentication - [More Details](https://developer.salesforce.com/docs/atlas.en-us.mc-app-development.meta/mc-app-development/integration-considerations.htm)

Diff for: setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
readme = f.read()
55

66
setup(
7-
version='1.2.0',
7+
version='1.3.0',
88
name='Salesforce-FuelSDK',
99
description='Salesforce Marketing Cloud Fuel SDK for Python',
1010
long_description=readme,

0 commit comments

Comments
 (0)