Office 365 APIs and Python Part 1: OAuth2
Knowing absolutely nothing about Python (other than it is apparently named after Monty Python!), I've decided to build a Python web app that integrates with the Office 365 APIs, specifically, the Contacts APIs. For your amusement, I'll be chronicling my misadventures as I learn Python and figure out how to use it to call the Office 365 APIs. If you're new to Python, let's learn together! If you're a Python veteran, please feel free to send me pointers or correct my mistakes in the comments.
Getting Started
When I decided to do this, I knew that I wanted to do a web app. From what I gathered on https://www.python.org, I'd need a web development framework. I settled on Django, and as it turns out, that was a great choice. Django really made this project easy, even for a Python newbie. Their documentation is great and had pretty much everything I needed.
Installation
Installing Python and Django on my Windows 8.1 laptop was easy and painless. I followed the installation guide and installed Python 3.4.2 and Django 1.7.1. The only thing that I needed to do that wasn't in the guide was add my Python install directory (C:\Python34) and the Scripts subdirectory (C:\Python34\Scripts) to my PATH environment variable.
Finding my way
After I was set up, I dove right into this awesome Django tutorial. I went through it all the way to the end, and it was fun! It touched on a lot of the cool features of Django and I had a working app in a couple of hours. Not bad. If you're new to Django, I highly recommend this tutorial.
Once I had completed the tutorial, it was time to start coding my own app. I set the goal of getting the OAuth2 authorization code grant flow working. I figure if I can figure that part out, the rest should be easy. Following along with the steps I learned from the tutorial, I created a new Django project with the highly-imaginative name of "pythoncontacts".
django-admin.py startproject pythoncontacts
Implementing OAuth2
At this point, I stopped to consider how I wanted to handle authorization and access tokens. Django is very database-centric, with each project having its own database associated with it. Django also has a user login framework, with user accounts being created and stored in the project's database. So I decided to go with the approach of having a "Django user" on the site connect their Office 365 account. A user would visit the site, login with their site-specific username and password, then connect their Office 365 account via the OAuth2 flow. The app stores the connection information in the database and associates it with the Django account.
Models
So with that in mind, I came up with the following model (Django's "thing which lives in the database"):
class Office365Connection(models.Model):
# The local username (the one used to sign into the website)
username = models.CharField(max_length = 30)
# The user's Office 365 account email address
user_email = models.CharField(max_length = 254) #for RFC compliance
# The access token from Azure
access_token = models.TextField()
# The refresh token from Azure
refresh_token = models.TextField()
# The resource ID for Outlook services (usually https://outlook.office365.com/)
outlook_resource_id = models.URLField()
# The API endpoint for Outlook services (usually https://outlook.office365.com/api/v1.0)
outlook_api_endpoint = models.URLField()
When a user connects their Office 365 account, the app will create an Office365Connection record in the database for them. The next time the user visits the site, they won't have to go through the connection process again, as long as the refresh token is still valid.
Views
The next thing to do is come up with the views for the app. I wanted to keep things simple, so I started with the index view for the contacts app. This is what the user will see if they browse to https://hostname/contacts. The index view should show data from Office 365, so we want to make sure that a user is logged in so we have a Django account. One thing that the Django tutorial didn't cover was handling login/logout. However, I found this tutorial on effectivedjango.com which pointed me in the right direction. By using the @login_required decorator I was able to make the index view require a logged in user. It will redirect to the login page if the user's not already logged in. I also used their example login and logout pages.
The index view looks for an Office365Connection for the user in the database. If there is one, it checks to see if there's already an access token. If there isn't, it uses the refresh token to obtain one. It then passes the access token to the view template for display. If there isn't one, it passes None to the view template, which will then prompt the user to connect their Office 365 account.
That prompt will take users to the connect view. This one is pretty simple, it just generates the authorization URL (login.windows.net) and redirects the user there. The user signs in and is presented with the consent page. Once the user consents (or cancels), their browser is redirected to the app's third view, the authorize view.
The authorize view uses the authorization code provided in the redirect to obtain an access token for the Discovery Service. It then calls the Discovery Service to find the API endpoint for the Contacts API. It also stores the refresh token in the database. That way, we can request an access token for the Contacts API later, without having to re-prompt the user. Finally, the view redirects the user to the index view.
Communicating with the OAuth2 endpoints
To implement all the actual work of making HTTP request to the OAuth2 endpoints, I created a separate module called o365service. I found the requests module, which was exactly what I needed to send requests. The o365service module provides methods to do the initial authorization, do discovery, and get access tokens from refresh tokens.
Overall impressions
I'm really pleased with how this effort has gone so far. Django is super easy to pick up and seems very well suited for doing OAuth. I'm confident that adding calls to the Contacts API is going to be a breeze. I'll tackle adding that to this sample in part 2, so stay tuned!
Grab the sample on GitHub, and let me know what you think on Twitter (@JasonJohMSFT).
Comments
Anonymous
January 09, 2015
Hey Jason, I'm curious if you've seen this. http://pytools.codeplex.comAnonymous
January 09, 2015
Yep! I actually played with this some, and it's pretty nice. If you're developing with Visual Studio, it's a great option. I ultimately decided not to use it for my little experiment to force myself to learn more about the inner workings of setting up Python and Django.Anonymous
May 19, 2015
Hi Jason, I have played around with your django application and thank you as it helped answer many of my questions and problems. But could you please point me in the right direction as to make this exact implementation work without the user granting access in the browser? I want a python script to create and delete events on behalf of the user/users (our own vacation application that needs to update users calendars after their leave is approved). I had everything implemented and working in basic authentication mode until I tried to delete an event. I could delete my own but not another users event even though I created it, I have been googling for almost 4 days now without any luck as there is limited documentation and examples when it comes to the REST Api. Thank you.Anonymous
May 19, 2015
Karol: Basic authentication is not the supported auth scheme for using the REST APIs. While it works today for some functionality, it will be disabled in the future. To get full functionality of the APIs, you need to use OAuth. If you want to avoid having individual users grant access to the app, you may want to look at the client credentials OAuth flow. In this flow, an admin grants access one time, and then the app has access to all mailboxes in the organization. Here's a blog about it: blogs.msdn.com/.../building-demon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow.aspx. I also did a Python example: blogs.msdn.com/.../client-credentials-flow-for-mail-api-in-python.aspxAnonymous
May 19, 2015
Hi, Thank you for your quick response. I have looked at your code with the client credential flow. Unfortunately this is still a show login -> get access model. I do not want to interact with any users or admin users at all. I want my flow to look something like this :
- script start (a normal python script that gets run by crontab)
- script queries my own local databases and finds 5 new events it needs to add
- request authorization code <-- I do not want this step to be done in a browser!!!!!!
- request access token
- create events
- close session
- close script I am not sure if this makes more sense. I have created and uploaded a x.590 certificate via the manifest upload and I would expect to authenticate using this certificate and not with human interaction. Is this even possible with your api? Funny thing...while writing this message to you I thought of something I have not tried and what do you know....it works :) This whole time my "resource" parameter in my token post was my application name and it was suppose to just be 'outlook.office365.com/&. Thank you for your help. ps: Your client credential "get_client_assertion" function really helped. :) So thanks.
Anonymous
May 19, 2015
Karol: Yes, the admin does have to login, but that actually only has to happen once, so that the admin can give their consent. Once consent has been granted, the app can request tokens silently. Glad you got it working!Anonymous
October 19, 2015
Hi Jason Johnston, I'm totally newbie to python and i have no understanding in django, recently i purchase office 365 and i want to integrate office 365 calender to odoo (formally openerp) but i have no idea how to use oauth2 using python without using django as odoo have own web framework and postgre sql, so can you please have me to integrate office 365 with odoo. Any inputs would be appreciated.Anonymous
September 19, 2016
Hi jason i want to upload a file to my sharepoint of office365 through a python script how can i achieve that can you help me ?- Anonymous
September 26, 2016
Sorry Sady, I don't know much about SharePoint!
- Anonymous
Anonymous
September 22, 2016
Jason, I am having difficulty getting your example to work. I was wondering if you could troubleshoot this. I am getting this error message after I connect to my office365 account, "An exception occurred: ["KeyError: 'Contacts_resource_id'\n"]".- Anonymous
September 26, 2016
Sorry to hear Beth. Can you open an issue on GitHub? https://github.com/jasonjoh/pythoncontacts. That way you can provide more info :)
- Anonymous