Implementing Docusign Authentication using Requests - python

I'm creating an Airbyte connector for Docusign's E-signature Rest API.
Part of the process of implementing a connector is to write an authentication routine that extends the AuthBase class from requests.auth.
The issue is that Docusign does not support refresh tokens for JWT grants. According to the docusign documentation:
The access token granted by JWT Grant expires after one hour, and no refresh token is provided. After the token expires, you must generate a new JWT and exchange it for a new access token.
You can reuse most of the old assertion, just modifying the IAT and EXP values and updating the signature, then submit the updated JWT to get a new access token.
Generally, apps that use JWT should get a new access token about 15 minutes before their existing one expires.
However, all of the examples in the "backend application flow" from this part of the requests documentation (which links to this page in the requests-authlib docs) only seem to allow an Auth2 workflow that includes a refresh token.
How can I work around this to make it so that, each time a refresh token expires, a new request is made (with updated IAT EXP, and signature)?

Refresh tokens are a feature of the OAuth Authorization Code grant flow.
The Authorization Code grant flow requires a human to authenticate themself. The result is an 8 hour access token and a 30 day refresh token.
To obtain a new access token, either:
Ask the human to authenticate again
Or the app can use the refresh token to obtain a new access token. This can be done autonomously by the app, without bothering the human.
For the JWT grant flow, there is no human and no refresh token. Instead, the app simply re-runs the JWT grant flow and receive a new 1 hour access token.
When you re-do the JWT flow, create a new JWT (updated IAT, EXP, etc). Sign it with your private key, and send it to DocuSign to obtain a new access token.
The JWT operation is cheap enough to do once per hour per impersonated user. But you must cache the access token and not re-do the JWT grant flow for each API call...
Python authentication libraries
Most authentication libraries for most languages focus on the Authorization Code grant flow since that is the most commonly used OAuth flow.
But as you've pointed out, you're using the JWT flow. This means that you cannot use these libraries. Instead, you will need to roll your own. Good news is that it isn't too hard. Here's my pseudo code:
Send_an_API_request(url, request_details, etc):
access_token = Get_access_token(user_id);
api_results = send_api_request(access_token, url, request_details, etc)
return api_results;
Get_access_token(user_id):
(access_token, expire_time) = database_lookup(user_id);
# if access_token is about to expire or we don't have one,
# create a new access_token and store it
if (
((current_time + 10minutes) > expire_time)
or
(access_token is null)
):
# Make a new JWT request
jwt = make_jwt(user_id);
signed_jwt = sign(jwt, private_key)
(access_token, expire_sec) = send_jwt_request(signed_jwt)
database_store (user_id, access_token, current_time + expire_sec)
return access_token
Added
Re:
[I need to] extend the AuthBase class from requests.auth
If the app's architecture requires you to extend the AuthBase class, then you will need to implement the JWT grant flow within the AuthBase class.
If the AuthBase class doesn't give you access to the data you need for the JWT grant flow, then a hack is to stuff the needed data into an available attribute such as the "refresh token."

Related

Power BI Rest API Requests Not Authorizing as expected

I have built a python application to access read only Power BI Rest API’s. I am automating the collection of tenant activity. However despite configuring my Azure App and using the service principal to generate an access token, the response I receive from the API request is one of an unauthorised response:
{"error": {"code": "PowerBINotAuthorizedException", "pbi.error": {"code":
"PowerBINotAuthorizedException", "parameters": {}, "details": [], "exceptionCulprit": 1}}}
I have found a number of similar issues posted online, however feel that I have done everything that is suggested but am still not able to get it working. I would appreciate any guidance.
The steps that I have taken are:
Configured an Azure App, adding the Application Permission for Power Bi Service-Tenant.Read.All
Screenshot of App Settings in Azure Portal
Requested my access token based upon the Client Credentials Flow using my app's client_ID and client_Secret as documented in the below link:
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
I successfully receive a token using the script below:
import requests
azureTenantID = "xxxxxxxxxxxxxxxxx"
azureClientId = "xxxxxxxxxxxxxxxxx"
azureClientSecret = "xxxxxxxxxxxxxxxxxx"
url = f"https://login.microsoftonline.com/{azureTenantID}/oauth2/v2.0/token"
payload = {
"grant_type": "client_credentials",
"client_id": azureClientId,
"client_secret": azureClientSecret,
"scope": "https://analysis.windows.net/powerbi/api/.default"
}
# Header HAS to be x-www-form-urlencoded for MS to accept it.
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
# Return POST content as JSON.
r = requests.post(url, data=payload, headers=headers).json()
# Grab the access token.
response = r.get("access_token")
# Concatenate with Bearer string
access_token = "Bearer {r['access_token']}"
Configured my Power BI Tenant Settings to enable Service Principals to use API's.
Screenshot of Admin API Setting
Screenshot of Developer API Setting
Note that I added the Service Principal as a member of the Security Group for which both of these settings are enabled
Execute my Get request to the API
The followings script returns a good response when I take an access token from the Power BI REST API Documentation's 'Try it out' feature, but not when I generate the token as above.
import requests
# Initialise parameters.
url = "https://api.powerbi.com/v1.0/myorg/admin/groups?$top=1000&$expand=datasets,dataflows,reports,users,dashboards"
headers = {'Authorization': get_access_token2()}
# Get response.
response = requests.get(url, headers=headers)
response = response.json()
Any assistance would be appreciated !
I just went through this exact scenario that you described, and in the end we had to engage Microsoft support to solve it.
Although extremely counter intuitive, if the app that you create for your service principal authentication has any Power BI permissions assigned to it then the access token that is generated (when passed to Power BI REST Admin API) will return an error response that reports PowerBINotAuthorizedException.
To be even more specific, if the access token that you pass to the Power BI API has a roles key/value pair, then you will get a PowerBINotAuthorizedException.
In your case, the issue is easier because you have listed out what permissions you granted. You mentioned that you Configured an Azure App, adding the Application Permission for Power Bi Service-Tenant.Read.All. In order to resolve this issue, you will need to remove that permission.
For future readers, you can troubleshoot this by decoding your access token using a JWT token decoder like one found at jstoolset.com. If your app has permissions allocated to the scope that you have requested (https://analysis.windows.net/powerbi/api/.default is the typical Power BI scope that you request in your authorization) and you decode your JWT token then you will see a roles key/value pair. The presence of this roles is essentially the issue. It does not matter that the values there might match up to the Required Scope in the Power BI REST Admin API documentation. It was described to us as if there is a roles value in your access token then when the token is presented to the Power BI API the roles that are granted are attempted to be used, which ultimately results in a PowerBINotAuthorizedException because service principals are not allowed to use a certain role.
If you have an app that you have removed all permissions from, but still has a value coming through in your access token for the roles key/value pair, then I would suggest starting with a new app with no permissions allocated to it, and simply add the new app to the existing security group that you originally created. This is how we realized that this truly was the issue, and were then able to reconcile from there.
EDIT: Microsoft has now updated their API documentation on the relevant endpoints to reflect this information. For example, in Admin - Groups GetGroupUsersAsAdmin the Required Scope now reads:
Tenant.Read.All or Tenant.ReadWrite.All
Relevant only when authenticating via a standard delegated admin access token. Must not be present when authentication via a service principal is used.

Python Uber SDK does not update access token after 30 days based on refresh token

I've created a script for Uber and it worked fine until my access token expired.
So here's this piece of code (almost similar to Uber SDK https://github.com/uber/rides-python-sdk):
session = Session(oauth2credential=OAuth2Credential(
client_id=credential_dict.get('client_id'),
access_token=credential_dict.get('access_token'),
expires_in_seconds=credential_dict.get('expires_in_seconds'),
scopes=credential_dict.get('scopes'),
grant_type=credential_dict.get('grant_type'),
redirect_url=credential_dict.get('redirect_url'),
client_secret=credential_dict.get('client_secret'),
refresh_token=credential_dict.get('refresh_token')))
client = UberRidesClient(session)
With the expired token I can not do anything further, it returns
uber_rides.errors.ClientError: 401: No authentication provided.
Also, I'm confused by "The SDK will handle the token refresh for you automatically when it makes API requests with an UberRidesClient."
How can I get my new access token using refresh token? I can authorize again and it works but is annoying.
You can get new access token if you have valid refresh token by using the token endpoint: https://login.uber.com/oauth/v2/token. For more information check the Uber documentation.
"When the user’s access_token has expired, obtain a new access_token by exchanging the refresh_token that is associated with the access_token using the Token endpoint".

How to extend my facebook graph API token in python?

I´m currently mining facebook using python. Until now, I´m getting an access token for graph API version 1.0(I need it to be 1.0) via this website:
https://developers.facebook.com/tools/explorer/?method=GET&path=me&version=v1.0
but the token I get on this site expires in about 1 hour, so how can I extend this token in python? I already saw how to in php, but, unfotunately, I have to stick with only python for my project
Facebook has 4 types of tokens: user, app, page and client (https://developers.facebook.com/docs/facebook-login/access-tokens#usertokens). Each of these token types have their own permissions. For example, you can post on behalf of a user (once they've granted you permission) using the app access token but need a user access token to pull user/statuses. The web user access token are short-lived (~1-2 hrs) and need to be converted to a long-lived (60 days) access token.
In python you can extend the token server-side calling the GraphAPI extend_access_token method assuming you are using the facebook-sdk (pip install facebook-sdk).
views.py:
import facebook
user_access_token = 'USER_ACCESS_TOKEN' # initially generated client-side
app_id = 'YOUR_UNIQUE_APP_ID' # found at developer.facebook.com
app_secret = 'YOUR_APP_SECRET' # found at developer.facebook.com
# Create fb graph object
graph = facebook.GraphAPI(user_access_token)
# Now extend it with the extend_access_token method
extended_token = graph.extend_access_token(app_id=app_id, app_secret=app_secret)
# Confirm token now expires in ~ 60 days or 5184000 seconds
print extended_token
Now that the token is extended, you can store and use on the users behalf w/out an active web client.

Google Cloud Endpoints authentication using webapp2 sessions

The client of my Google Cloud Endpoints API is an JavaScript (AngularJS) web application hosted on the same Google App Engine application as the Endpoints API itself. My users authenticate using webapp2 sessions (datastore). They don't necessarily have a Google account. I want to be able to do a request to the Endpoints API like /api/users/me which would return the user data of the user who is currently logged in.
First I thought I had to implement a OAuth2 provider for my App Engine application, and then let the AngularJS application request a OAuth2 access token from my own App Engine OAuth provider (instead of the OAuth provider of Google, like the built in authentication mechanism does).
However, this comment suggests not implementing my own OAuth2 provider but instead providing arbitrary parameters in my request (in a message field, or in a HTTP header) to the Endpoints API. I guess that parameter should be a user token (some encrypted value unique to the logged in user?). That value should then be passed to the browser. Isn't that insecure? I would like not to serve my AngularJS application on HTTPS if possible (to save costs).
Is this a good use case for OAuth2? Or is OAuth2 only for granting third party applications access to user data?
In case OAuth2 is not the way to go: how to pass a user token securily to the browser and prevent man-in-the-middle attacks? Should the user token expire after a certain amount of time?
I've just finished implementing exactly what you've described. Basically this method does the trick:
def get_current_session(request_state):
cookies = werkzeug.http.parse_cookie(request_state.headers.get('Cookie'))
sess_cookie = cookies.get('mc_session')
parts = sess_cookie.split('|')
if len(parts) != 3:
logging.error('Cookie does not have 3 parts')
return False
signature = hmac.new(COOKIE_SECRET_KEY, digestmod=hashlib.sha1)
signature.update('|'.join(parts))
sig_hex = signature.hexdigest()
if compare_hashes(sig_hex, parts[2]):
logging.error('Cookie signature mismatch!')
return False
cookie_data = webapp2_extras.json.b64decode(parts[0])
return sessions_ndb.Session.get_by_sid(cookie_data['_sid'])
And you'd call that from your API method using:
session = get_current_session(self.request_state)
You can find all the details at: https://blog.artooro.com/2014/08/21/share-sessions-between-google-cloud-endpoints-and-webapp2/

Authentication with the Google Docs List API, Python and OAuth 2

I'm trying to use the Google Docs API with Python+Django and OAuth 2. I've got the OAuth access token, etc. via google-api-python-client, with the code essentially copied from http://code.google.com/p/google-api-python-client/source/browse/samples/django_sample/plus/views.py
Now, I assume I should be using the google gdata API, v 2.0.17. If so, I'm unable to find exactly how to authorize queries made using the gdata client. The docs at http://packages.python.org/gdata/docs/auth.html#upgrading-to-an-access-token (which appear outdated anyway), say to set the auth_token attribute on the client to an instance of gdata.oauth.OAuthToken. If that's the case, what parameters should I pass to OAuthToken?
In short, I'm looking for a brief example on how to authorize queries made using the gdata API, given an OAuth access token.
The OAuth 2.0 sequence is something like the following (given suitably defined application constants for your registered app).
Generate the request token.
token = gdata.gauth.OAuth2Token(client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
scope=" ".join(SCOPES),
user_agent=USER_AGENT)
Authorise the request token. For a simple command-line app, you can do something like:
print 'Visit the following URL in your browser to authorise this app:'
print str(token.generate_authorize_url(redirect_url=REDIRECT_URI))
print 'After agreeing to authorise the app, copy the verification code from the browser.'
access_code = raw_input('Please enter the verification code: ')
Get the access token.
token.get_access_token(access_code)
Create a gdata client.
client = gdata.docs.client.DocsClient(source=APP_NAME)
Authorize the client.
client = token.authorize(client)
You can save the access token for later use (and so avoid having to do the manual auth step until the token expires again) by doing:
f = open(tokenfile, 'w')
blob = gdata.gauth.token_to_blob(token)
f.write(blob)
f.close()
The next time you start, you can reuse the saved token by doing:
f = open(tokenfile, 'r')
blob = f.read()
f.close()
if blob:
token = gdata.gauth.token_from_blob(blob)
Then, the only change to the authentication sequence is that you pass this token to OAuth2Token by specifying a refresh_token argument:
token = gdata.gauth.OAuth2Token(client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
scope=" ".join(SCOPES),
user_agent=USER_AGENT,
refresh_token=token.refresh_token)
Hope this helps. It took a while to work it out :-).
This is from https://developers.google.com/gdata/docs/auth/overview:
Warning: Most newer Google APIs are not Google Data APIs. The Google Data APIs documentation applies only to the older APIs that are listed in the Google Data APIs directory. For information about a specific new API, see that API's documentation. For information about authorizing requests with a newer API, see Google Accounts Authentication and Authorization.
You should either use OAuth for both authorization and access or OAuth 2.0 for both.
For OAuth 2.0 API are now at https://developers.google.com/gdata/docs/directory.

Categories

Resources