Python: OneDriveTokenHandler (Microsoft Graph API)
Introduction
Right, so.
I’ve pulled away from MSAL for my little project which is a small CLI interface into my OneDrive for Linux. Thing is, I want it to be light-weight and MSAL comes with a lot of dependencies. It’s a one-for-all library that can connect to a number of different Microsoft cloud accounts, both business and consumer. I’m only interested in consumer and, specifically, OneDrive. This doesn’t reflect my opinion on MSAL. I think it’s pretty good, but I don’t need all the bells and whistles and the bloat and the behind-the-scenes stuff it’s doing that I’m not seeing. I just want to interface with my personal OneDrive using Linux.
How can I go about this?
First, a rename to OneDriveTokenHandler
which makes more sense and, hopefully, better describes what the class is designed for.
Microsoft Graph Authorisation Process
With a bit of research around the Microsoft Graph documentation (which is pretty sketchy, to say the least) I finally worked out how to get hold of a OneDrive access token that can be used to manipulate OneDrive without needing to install and import MSAL. The process, as described in (slightly) more detail here, is outlined below:
Open a web-browser at the Microsoft consumer login end-point (I use the standard Python
webbrowser
library) and pass specific details about your application as URL parameters.User is then prompted to log in to their Microsoft 365 account as usual (or may not need to if their login is already cached by the browser).
User accepts the application permissions required (e.g. my application will need to read and write to OneDrive).
Microsoft processes the logon and the browser is redirected to the
redirect_uri
which is configured when the app is initially registered by the developer. For my purposes (and for most consumer applications) theredirect_uri
ishttp://localhost
. This will be the client machine running the application.The application must be ready and listening for the above redirect which will be in the form of a standard HTML POST. It must catch and parse the URL which will include either an authorisation code or an error. An HTTP status of 200 is then returned, along with a string advising the user it’s safe to close the browser or even an error message, depending on the response from Microsoft Graph.
The authorisation code received in the above step can then be sent to the Microsoft token end-point and redeemed for access and refresh tokens.
From this point on, everything is the same as the previous version. Because we persist refresh tokens into a Sqlite3 database so they can be used again the app should never need to re-authenticate unless the refresh token expires (at the time of writing this is 90-days). Additionally, new refresh tokens are always sent with new access tokens so if the application is used regularly and its Sqlite3 database is never deleted, there should never be a need to re-authenticate on the same client.
The biggest complication is around accepting the authorisation token so it can be redeemed for our initial access and refresh tokens. Obviously, something needs to be listening locally for the http POST request that will come from our browser. A bit of a rummage around the MSAL source showed there wasn’t really much trickery required, here, just an awareness of some vulnerabilities and ways to mitigate them.
TinyAcceptorHTTPServer
Embedded in the new OneDriveTokenHandler class is the TinyAcceptorHTTPServer
class. This is a very small, very basic HTTP server that extends the well-known Python HTTPServer
class with a couple of properties of its own.
Property | Type | Description | ||
---|---|---|---|---|
auth_code | string | The authorisation code to be returned. | ||
state | string | When we submit a request to Graph for an authorisation token we can supply a string called state as a parameter. The Graph API sends this state value back with the authorisation code for comparison. This property is set once the initial authorisation request is sent so it can be compared with the response when it’s received by the HTTPServer superclass. |
Obviously, starting a listening web-server on your local client machine comes with some risks, but the following precautions should go some way to mitigate them:
- The port it listens on will be random (chosen by the OS)
- It doesn’t seem to be documented, but the Microsoft Graph API permits you to use a port with the
redirect_uri
property, as I discovered when hunting through the MSAL source.
- It doesn’t seem to be documented, but the Microsoft Graph API permits you to use a port with the
- We wait for one HTTP request and one HTTP request only, then close the server
- We check the
state
value received in the result matches the one we sent with the original authorisation request (see properties above) - After a timeout (default 3 minutes) we close the server with an error
- It’s tempting to put a lower timeout but, remember, you need to give the user time to enter their credentials then read and accept the warning that they’re about to give permissions to the app.
When we open the web-browser to display Microsoft’s logon page we immediately start the TinyAcceptorHTTPServer
listening on localhost
or 127.0.0.1
and allow the operating system to assign it a random port by providing a port
parameter of 0
. This port is cached in the server_port
property of the HTTPServer
object.
The server waits to process one request and one request only, assuming it to be the authorisation code from Microsoft or an error response. Once either is returned, a 200 status code is sent and the server is shut-down. The authorisation code or error is cached as a property in the server object for later retrieval.
Now the authorisation code can be redeemed for a token by sending it to the Microsoft token end-point.
Getting a token
As with the previous version, all you need to do is initialise the class, passing it the relevant parameters, then call get_token()
. If you do not have an unexpired token in the class’s cache the it will attempt to load a refresh token from its database. If a refresh token is not availble, then it will run users through the authentication process as described above.
Github Link
Source can be found on GitHub, here: https://github.com/chris-j-akers/OneDriveTokenHandler
Example Usage:
1
2
3
4
5
6
7
8
import os
from OneDriveTokenHandler import OneDriveTokenHandler
token_handler = OneDriveTokenHandler(app_name='my-application',
client_id='12345678-abcd-1234-abcd-12345678',
scopes=['Files.ReadWrite.All', 'offline_access'],
db_filepath=os.path.expanduser('~/.local/share/my-application/settings_db')
new_token = token_handler.get_token()
Parameters
Parameter | Req | Type | Description | |
---|---|---|---|---|
app_name | Y | string | A name for your app. This is unique to OneDriveTokenHandler and really just used when the refresh-token is persisted so can be anything. It doesn’t need to match the name of your app as it’s registered in Azure, for instance. | |
client_id | Y | string | ClientId (also called AppId) of your app. This is generated once you’ve registered your App in Azure and can be retrieved by logging onto the Azure Portal and selecting App Registrations. Clicking on your registered app will show the Application (client) Id field in the Essentials section. Just copy this value. | |
scopes | Y | [string] | A list of scopes that must be enabled for the access token (NOTE: if offline_access is not included in this list it’s automatically added as it’s required to retrieve a refresh token). | |
db_filepath | N | string | (optional) Specifies the location/name of the Sqlite3 db database that stores the refresh token. Defaults to ./tokens.db . |