29 September 2018

Using Azure Active directory as an OAuth2 provider for Django

Azure Active Directory is a great product and is invaluable in the enterprise space. In this article we'll be setting it up to provide tokens for the OAuth2 client credentials grant. This authorization flow is useful when you want to authorize server-to-server communication that might not be on behalf of a user.

This diagram, by Microsoft, shows the client credentials grant flow.
From Microsoft documentation
 The flow goes like this:
  1. The client sends a request to Azure AD for a token
  2. Azure AD verifies the attached authentication information and issues an access token
  3. The client calls the API with the access token. The API server is able to verify the validity of the token and therefore the identity of the client.
  4. The API responds to the client

Setting up Azure AD as an OAuth2 identity provider

The first step is to create applications in your AD for both your API server and the client. You can find step-by-step instructions on how to register the applications on the Microsoft page at Integrating applications with Azure Active Directory.

We need to generate a key for the client. From your AD blade in the Azure portal click on "app registrations" and then show all of them. Select the client application and then open it's settings. Create a new key and make sure you copy its value after saving.

Now let's add the permissions that our client will ask for. For this article we're just going to use a basic "access" scope. We're not going to add custom scopes to the server application, but this is easily possible by editing the manifest of the server application.

From the settings page of the client application click on "Required permissions". Click "Add" and in the search box type in the name of the server application that you registered in AD. Select it from the list that appears and choose "access" under the delegated permissions.

Now we have a fully fledged OAuth2 service set up and running, which is awesome. Let's call the Azure AD endpoint to get a token:

You can get the application id by opening up the registered application from your Azure AD portal blade.

Azure should respond with a JSON body that looks something like this:


The access_token value will be a JWT token. If you haven't used them before go check out jwt.io. This is the access token that you will present to your Django backend.

If you copy the access token into the decoder on jwt.io then you'll see that the payload looks like this:

Interlude

Let's take a breather and watch a music video. Because, really, 8 bits should be enough for anybody.

Okay, we're back and we're ready to get Django to authenticate the token.

Validating the token in Django

I'm not going to go into the details of setting up your Django project. There is already an excellent tutorial on their site. I'm going to assume that you have an app in your project to handle the API.

What we need to do is write some custom middleware for Django that will get the access token out of the "Authorization" HTTP header, validate the JWT, and check the signature against Azure's published signature. You can find out more about this on Microsoft's website, but this is actually an OAuth2 standard approach so just about any standards compliant documentation will do.

We'll be using standard libraries to decode the JWT, but in order to properly verify it we will need to have the key. We don't want to hardcode the key into our API because Azure rotates their keys on a regular basis. Luckily the OAuth2 standard provides a defined way for an OAuth2 provider to share this sort of information.

Azure publishes it's OAuth2 configuration file at https://login.microsoftonline.com/common/.well-known/openid-configuration. There is also a configuration file that is specific to your tenant at https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration, but the general one is fine for us.

In that document there is a key called "jwks_url" whose value is a url. On that url you will find the keys that Azure is currently using. We need to retrieve that document and find the key that was used in our access token.

Right, so that's the plan. Decode the JWT to find what key was used to sign it and go fetch the key from Microsoft to verify the token. If anybody is able to trick our DNS and serve us content that we think is from Microsoft then they'll be able to pwn us, but otherwise as long as we trust the response from Microsoft we should be able to trust the JWT token the client gave us.

I placed this into settings.py because swapping Azure AD for any other standard implementation of OAuth2 should be possible by just swapping the configuration endpoint. I also set up Memcached while I was busy in the settings, so I ended up with this extra settings information:


Then I made a new file called checkaccesstoken.py and put it into the "middleware" directory of my API app, making sure that I registered it in settings.py

In the __call__ method of the middleware I call out to the functions that are going to check the token:

To get the token we use the Django request object:

We make sure that the token is a Bearer token by defining a class constant called AUTH_TYPE_PREFIX as having the value "Bearer".

Now that we have the access token we need to check it. You could use any JWT library, but I chose to use "pip install pyjwt cryptography". This JWT library will throw a DecodeError if the token is invalid, which we catch and respond with 401 to the user.

The public key is available over the network so we want to cache it to reduce the I/O time. Microsoft suggests that it is safe to cache for about a day.

Now we're done! We can send a bearer access token to our API and be authenticated. Note that we're not doing any authorization, all we're doing here is authenticating. We haven't tied this client application to any form of role in our application. That's for another day :)

References:
  1. I placed the whole middleware file in a Gist
  2. Microsoft article on client credentials flow
  3. Microsoft advice on validating tokens

No comments:

Post a Comment

Note: only a member of this blog may post a comment.