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

Commit

Permalink
Refresh Token, Web/Public App support (#116)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sfcbetiuc authored and manivinesh committed Aug 7, 2019
1 parent 34eb519 commit 40c9379
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 25 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ objsamples/ET_Client.py
objsamples/config.python
objsamples/*.pyc
config.python
FuelSDK/Public_WebAppTests/config.python

soap_cache_file.json
.idea/
.idea/
venv
Empty file.
15 changes: 15 additions & 0 deletions FuelSDK/Public_WebAppTests/config.python.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[Web Services]
appsignature: none
clientid: <CLIENT_ID>
clientsecret: <CLIENT_SECRET>
defaultwsdl: https://webservice.exacttarget.com/etframework.wsdl
authenticationurl: <AUTH TENANT SPECIFIC ENDPOINT>
wsdl_file_local_loc: <WSDL_PATH>/ExactTargetWSDL.xml

[Auth Service]
useOAuth2Authentication: True
accountId: <TARGET_ACCOUNT_ID>
scope: <PERMISSION_LIST>
applicationType: <APPLICATION_TYPE>
redirectURI: <REDIRECT_URI_FOR_PUBLIC/WEB_APP>
authorizationCode: <AUTHORIZATION_CODE_FOR_PUBLIC/WEB_APP>
79 changes: 79 additions & 0 deletions FuelSDK/Public_WebAppTests/test_ET_Client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from unittest import TestCase
from FuelSDK import ET_Client


class TestET_Client(TestCase):

@classmethod
def setUpClass(cls):
"""
test_authToken_and_refreshKey_should_differ_if_refresh_token_is_enforced expects a Public/Web App config.
All the other tests require a Server to Server config
"""

cls.client = ET_Client(False, False)

def test_authToken_and_refreshKey_should_differ_if_refresh_token_is_enforced(self):
self.authToken1 = self.client.authToken
self.refreshKey1 = self.client.refreshKey

self.client.refresh_token_with_oAuth2(True)

self.authToken2 = self.client.authToken
self.refreshKey2 = self.client.refreshKey

self.assertNotEqual(self.authToken1, self.authToken2)
self.assertNotEqual(self.refreshKey1, self.refreshKey2)

def test_auth_payload_should_have_public_app_attributes(self):
self.client.application_type = 'public'

payload = self.client.create_payload()

self.assertEqual(self.client.client_id, payload['client_id'])
self.assertEqual(self.client.redirect_URI, payload['redirect_uri'])
self.assertEqual(self.client.authorization_code, payload['code'])
self.assertEqual('authorization_code', payload['grant_type'])

def test_auth_payload_for_public_app_should_not_have_client_secret(self):
self.client.application_type = 'public'

payload = self.client.create_payload()

self.assertRaises(KeyError, lambda: payload['client_secret'])

def test_auth_payload_should_have_web_app_attributes(self):
self.client.application_type = 'web'

payload = self.client.create_payload()

self.assertEqual('authorization_code', payload['grant_type'])
self.assertEqual(self.client.client_id, payload['client_id'])
self.assertEqual(self.client.client_secret, payload['client_secret'])
self.assertEqual(self.client.redirect_URI, payload['redirect_uri'])
self.assertEqual(self.client.authorization_code, payload['code'])

def test_auth_payload_should_have_server_app_attributes(self):
self.client.application_type = 'server'

payload = self.client.create_payload()

self.assertEqual('client_credentials', payload['grant_type'])
self.assertEqual(self.client.client_id, payload['client_id'])
self.assertEqual(self.client.client_secret, payload['client_secret'])

def test_auth_payload_for_server_app_should_not_have_code_and_redirect_uri(self):
self.client.application_type = 'server'

payload = self.client.create_payload()

self.assertRaises(KeyError, lambda: payload['code'])
self.assertRaises(KeyError, lambda: payload['redirect_uri'])

def test_auth_payload_with_refresh_token_should_have_refresh_token_attribute(self):
self.client.refreshKey = 'RefreshKey'

payload = self.client.create_payload()

self.assertEqual('refresh_token', payload['grant_type'])
self.assertEqual(self.client.refreshKey, payload['refresh_token'])
2 changes: 1 addition & 1 deletion FuelSDK/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '1.2.0'
__version__ = '1.3.0'

# Runtime patch the suds library
from FuelSDK.suds_patch import _PropertyAppender
Expand Down
96 changes: 79 additions & 17 deletions FuelSDK/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class ET_Client(object):
use_oAuth2_authentication = None
account_id = None
scope = None
application_type = None
authorization_code = None
redirect_URI = None

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

self.configure_client(get_server_wsdl, params, tokenResponse)

def configure_client(self, get_server_wsdl, params, tokenResponse):

## Read the config information out of config.python
config = configparser.RawConfigParser()

if os.path.exists(os.path.expanduser('~/.fuelsdk/config.python')):
config.read(os.path.expanduser('~/.fuelsdk/config.python'))
else:
Expand Down Expand Up @@ -148,8 +156,45 @@ def __init__(self, get_server_wsdl = False, debug = False, params = None, tokenR
elif "FUELSDK_SCOPE" in os.environ:
self.scope = os.environ["FUELSDK_SCOPE"]

if params is not None and "authorizationCode" in params:
self.authorization_code = params["authorizationCode"]
elif config.has_option("Auth Service", "authorizationCode"):
self.authorization_code = config.get("Auth Service", "authorizationCode")
elif "FUELSDK_AUTHORIZATION_CODE" in os.environ:
self.authorization_code = os.environ["FUELSDK_AUTHORIZATION_CODE"]

if params is not None and "applicationType" in params:
self.application_type = params["applicationType"]
elif config.has_option("Auth Service", "applicationType"):
self.application_type = config.get("Auth Service", "applicationType")
elif "FUELSDK_APPLICATION_TYPE" in os.environ:
self.application_type = os.environ["FUELSDK_APPLICATION_TYPE"]

if self.is_none_or_empty_or_blank(self.application_type):
self.application_type = "server"

if params is not None and "redirectURI" in params:
self.redirect_URI = params["redirectURI"]
elif config.has_option("Auth Service", "redirectURI"):
self.redirect_URI = config.get("Auth Service", "redirectURI")
elif "FUELSDK_REDIRECT_URI" in os.environ:
self.redirect_URI = os.environ["FUELSDK_REDIRECT_URI"]

if self.application_type in ["public", "web"]:
if self.is_none_or_empty_or_blank(self.authorization_code) or self.is_none_or_empty_or_blank(self.redirect_URI):
raise Exception('authorizationCode or redirectURI is null: For Public/Web Apps, the authorizationCode and redirectURI must be '
'passed when instantiating ET_Client or must be provided in environment variables/config file')

if self.application_type == "public":
if self.is_none_or_empty_or_blank(self.client_id):
raise Exception('clientid is null: clientid must be passed when instantiating ET_Client or must be provided in environment variables / config file')
else: # application_type is server or web
if self.is_none_or_empty_or_blank(self.client_id) or self.is_none_or_empty_or_blank(self.client_secret):
raise Exception('clientid or clientsecret is null: clientid and clientsecret must be passed when instantiating ET_Client '
'or must be provided in environment variables / config file')

## get the JWT from the params if passed in...or go to the server to get it
if(params is not None and 'jwt' in params):
if (params is not None and 'jwt' in params):
decodedJWT = jwt.decode(params['jwt'], self.appsignature)
self.authToken = decodedJWT['request']['user']['oauthToken']
self.authTokenExpiration = time.time() + decodedJWT['request']['user']['expiresIn']
Expand Down Expand Up @@ -203,7 +248,7 @@ def build_soap_client(self):

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

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

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

headers = {'content-type': 'application/json',
'user-agent': 'FuelSDK-Python-v1.2.0'}

payload = {'client_id': self.client_id,
'client_secret': self.client_secret,
'grant_type': 'client_credentials'
}
'user-agent': 'FuelSDK-Python-v1.3.0'}

if self.account_id is not None and self.account_id.strip() != '':
payload['account_id'] = self.account_id
if self.scope is not None and self.scope.strip() != '':
payload['scope'] = self.scope
payload = self.create_payload()

self.auth_url = self.auth_url.strip() + '/v2/token'
auth_endpoint = self.auth_url.strip() + '/v2/token'

r = requests.post(self.auth_url, data=json.dumps(payload), headers=headers)
r = requests.post(auth_endpoint, data=json.dumps(payload), headers=headers)
tokenResponse = r.json()

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

if 'refresh_token' in tokenResponse:
self.refreshKey = tokenResponse['refresh_token']

self.build_soap_client()

def create_payload(self):
payload = {'client_id': self.client_id}

if self.application_type != "public":
payload['client_secret'] = self.client_secret

if not self.is_none_or_empty_or_blank(self.refreshKey):
payload['grant_type'] = "refresh_token"
payload['refresh_token'] = self.refreshKey
elif self.application_type in ["public", "web"]:
payload['grant_type'] = "authorization_code"
payload['code'] = self.authorization_code
payload['redirect_uri'] = self.redirect_URI
else:
payload['grant_type'] = "client_credentials"

if not self.is_none_or_empty_or_blank(self.account_id):
payload['account_id'] = self.account_id
if not self.is_none_or_empty_or_blank(self.scope):
payload['scope'] = self.scope

return payload

def get_soap_cache_file(self):
json_data = {}
Expand Down Expand Up @@ -329,7 +391,7 @@ def get_soap_endpoint(self):
"""
try:
r = requests.get(self.base_api_url + '/platform/v1/endpoints/soap', headers={
'user-agent': 'FuelSDK-Python-v1.2.0',
'user-agent': 'FuelSDK-Python-v1.3.0',
'authorization': 'Bearer ' + self.authToken
})

Expand Down Expand Up @@ -387,4 +449,4 @@ def CreateDataExtensions(self, dataExtensionDefinitions):
def is_none_or_empty_or_blank(self, str):
if str and str.strip():
return False
return True
return True
8 changes: 4 additions & 4 deletions FuelSDK/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def __init__(self, auth_stub, endpoint, qs = None):
fullendpoint += urlSeparator + qStringValue + '=' + str(qs[qStringValue])
urlSeparator = '&'

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


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

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

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

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

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

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

obj = super(ET_DeleteRest, self).__init__(r, True)
Expand Down
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,64 @@
# FuelSDK-Python v1.2.0
# FuelSDK-Python v1.3.0

Salesforce Marketing Cloud Fuel SDK for Python

## Overview

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.

New Features in Version 1.3.0
------------
* Added Refresh Token support for OAuth2 authentication
* Added Web/Public App support for OAuth2 authentication

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)

Sample config:

```
[Web Services]
appsignature: none
clientid: <CLIENT_ID>
clientsecret: <CLIENT_SECRET>
defaultwsdl: https://webservice.exacttarget.com/etframework.wsdl
authenticationurl: <AUTH TENANT SPECIFIC ENDPOINT>
baseapiurl: <REST TENANT SPECIFIC ENDPOINT>
soapendpoint: <SOAP TENANT SPECIFIC ENDPOINT>
wsdl_file_local_loc: <WSDL_PATH>/ExactTargetWSDL.xml
[Auth Service]
useOAuth2Authentication: True
accountId: <TARGET_ACCOUNT_ID>
scope: <PERMISSION_LIST>
applicationType: <APPLICATION_TYPE>
redirectURI: <REDIRECT_URI_FOR_PUBLIC/WEB_APP>
authorizationCode: <AUTHORIZATION_CODE_FOR_PUBLIC/WEB_APP>
```

Example passing config as a parameter to ET_Client constructor:

```
stubObj = ET_Client.ET_Client(
False, False,
{
'clientid': '<CLIENT_ID>',
'clientsecret': '<CLIENT_SECRET>',
'defaultwsdl': 'https://webservice.exacttarget.com/etframework.wsdl',
'authenticationurl': '<AUTH TENANT SPECIFIC ENDPOINT>',
'baseapiurl': '<REST TENANT SPECIFIC ENDPOINT>',
'soapendpoint': '<SOAP TENANT SPECIFIC ENDPOINT>',
'wsdl_file_local_loc': r'<WSDL_PATH>/ExactTargetWSDL.xml',
'useOAuth2Authentication': 'True',
'accountId': '<TARGET_ACCOUNT_ID>',
'scope': '<PERMISSION_LIST>'
'applicationType': '<APPLICATION_TYPE>'
'redirectURI': '<REDIRECT_URI_FOR_PUBLIC/WEB_APP>'
'authorizationCode': '<AUTHORIZATION_CODE_FOR_PUBLIC/WEB_APP>'
})
```

* applicationType can have one of the following values: `server`, `public`, `web`. The default value of applicationType is `server`.

New Features in Version 1.2.0
------------
* 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)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
readme = f.read()

setup(
version='1.2.0',
version='1.3.0',
name='Salesforce-FuelSDK',
description='Salesforce Marketing Cloud Fuel SDK for Python',
long_description=readme,
Expand Down

0 comments on commit 40c9379

Please sign in to comment.