Part 5: Authorization Grant, Our First OAuth Dance Steps
This is part five of a tutorial blog series from Ben Finkel addressing the challenges, solutions, and implementation of sound authentication. By the end of this series, you will be confident in your ability to implement an authentication system — even with little-to-no background.
The first leg of the OAuth Dance is the Authorization Grant.* This is the phase where the user of our app gives the provider permission to share our data. We "grant" the requested "authorization," hence the name of this step. In this post we'll setup our OAuth library and expand our main application to take advantage of it.
Scaffolding Our App
It's important that we structure our library such that it is:
- Intuitive to parse and understand;
- Relatively easy to integrate with an application; and
- Still flexible enough to handle the vagaries of OAuth.
Here is a general overview of how we can do that. Our application-specific code will be kept in a main.py file. This will represent whatever app is implementing OAuth. Everything OAuth-specific will be imported via a single import line from OAuth.py.
OAuth.py will contain all of our OAuth-specific code. For any functions that are provider specific we will import a provider .py file into OAuth.py. If we code it right, adding a new provider should be as easy as copying an existing provider's .py file, making the necessary adjustments, and importing it into OAuth.py.
All of the code for this post is available at the public GIT repo linked at the end of the post.
The Consumer App
Our app.yaml file does not need to change from the previous post, here it is for reference:
Feel free to change the version if you'd like to have both the old and new versions running live in production. Otherwise when we deploy, the new copy will overwrite the previous copy.
Main.py has quite a few additions, let's cover it step-by-step.
We need to import OAuth (don't worry, we'll create it below) as well as a utilities library. The utilities library could be written directly in main.py, but for clarity I broke it out. More on that file in our next post.
After our imports, we've got two global constants.
- privateAppKey is, just like it says, a private app-specific key used for the creation of state tokens (more on that later) and our session store.
- redirect_uri is the url in our app that will handle the callback after the provider goes through the grant process. Note: this is in main.py since this uri will change from consumer to consumer app.
The next section of main.py creates a new class called BaseHandler. This is just a wrapper for webapp2.RequestHandler and allows us to create a session store that we'll use later. All of our subsequent router handlers will derive from BaseHandler. See documentation on webapp2's session store here.
The MainPage class has been updated to display a link to our SignIn router ("/signin") and pass the provider in the querystring. This allows you to choose which providers to expose to the user right in your HTML template.
Two additional classes have been created for handling routes. SignIn is the class that handles when the user wishes to sign into our app. OAuthCallback is the class that will handle the post-Grant callback from the provider to our app.
SignIn extracts the provider from the querystring (only 'google' is an option so far, but we'll be expanding), generates a state token (more on that in the next post) and then passes those two pieces of info, along with our redirect_uri constant, to the OAuth library. In return, it gets a URL which we then redirect to with self.redirect(authUrl).
Our final step in main.py involves setting up the configuration for the session store, expanding the routes in our app, and adding the config variable to the app object.
The OAuth Layer
Our OAuth.py file is initially very simple.
First, we import our provider specific OAuth libraries (just googleOAuth.py so far) and then create a map of provider strings to library functions. This allows us to programmatically call the proper function without an ever-growing Switch statement.
The implementation of the SignIn function is fairly straightforward. We lookup the proper function from the earlier map, based on the provider that was passed in, then call that function to get our URL, returning the resultant url back to the calling application (our consumer).
A Provider Implementation
This is the basic structure we'll use for all of our OAuth providers.
First we establish our auth endpoint with a constant. This will change for each provider. Additional endpoints will be stated here as we expand the functions of any given provider.
The second statement is our parameters collection that we'll need at various points in time during the OAuth process. They are based on a variety of different sources.
- Response_type — for our purposes this will always be 'code.' See RFC6749 for more details on how this is used.
- Client_id — this is our application-specific client_id generated during post #4 (see Step 3).
- Scope — this represents the permissions we are requesting. Initially I'm using the same calendar scope we used in the OAuth playground during Post #3. Eventually we'll want to replace this with something more specific**.
The SignIn function implementation here fills in the last two empty values in our parameters collection, state and redirect_uri. These are passed in from our consumer app via the OAuth.py library. We build an appropriate URL and pass it back.
You can now deploy this application to your appspot.com/ URL. Launch the App Engine Launcher and either run the site locally or deploy it to Google Cloud Platform (see the previous post for detailed instructions). Having done that, our website is a simple page with a link to sign in with Google. Following that link should redirect you to the Google OAuth landing page. If you're not already signed into Google, or if you have multiple accounts, you'll have to sign in.
Once you've signed in, you get an Kill! Don't worry, that's expected. Extra credit if you know why you got that Kill and how to correct it. We'll come back and resolve that issue in a little bit.
For now, rejoice! You've completed Step 1 of the OAuth Dance: The User Authorization Grant.
You can read the full tutorial series in these weekly installments:
Part 2: Delegating Authentication
Part 5: Authorization Grant, Our First OAuth Dance Steps
The series will conclude in April 2016.
* In a 2-legged OAuth flow the grant step still happens, it's just done ahead of time by the developer. The authorization token is produced and shipped with the application so it implicitly has this grant. That's why 2-legged OAuth is also known as Implicit Grant.
** Here you can see the first signs of OAuth-as-Authentication cracking. We need to browse the Google-specific library of scopes to find what we want. What we want may not even be offered. OpenID Connect solves this for us.