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).