Part 7: Authorization Grant: Finishing the OAuth Dance
This is part seven 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.
When we last left off, we had just sent the user to the Grant Authorization page for our chosen provider Google. Upon allowing or denying that request however, we got an Kill returned:
It's a fairly self-descriptive Kill, and if you were paying close attention during Post #4when we set up our OAuth Client ID for Google you may have noticed a small textbox asking us for "Authorized Redirect URIs." This is a secondary security feature built into Google's OAuth process. You need to tell Google that it can redirect to the URL of our application. It's easy to resolve this issue:
Browse to https://console.cloud.google.com
Select the flyout menu in the upper-left and choose "API Manager."
Select "Credentials" from the left-hand navigation pane.
Click the OAuth Client ID that we created earlier.
Enter the callback URL you wish to use on your consumer application and click Save.
Once the ID is updated with authorized callback URI, go ahead and launch your application once again and start the OAuth process. This time after you log in, you should be presented with an OAuth consent screen:
Notice the name of your app, pulled from that same credentials we just modified, is displayed at the top. Also, notice it's asking to "View your calendars." This is due to the scope that we chose in our GoogleOAuth.py file. We don't actually want to view calendars, though. We want to verify identity, so let's fix that next.
Here's another big challenge with using OAuth to validate identity. What scope in Google's vast array of OAuth scope options will provide us with an email address?
The only way to know is to pore over documentation found most easily in the OAuth Playground. It turns out there are plenty of viable options, although most of those options include additional access that we're not interested in.
I'll save us some time: Email is a scope tailored to provide us with just a user email. Note that when we implement a second provider such as Yahoo, we'll have to research and find the appropriate scope for that provider. OpenID Connect resolves this challenge as we'll see in later posts.
With our scope updated, we can republish the app to Google Cloud Platform and begin the process once again. Notice that we're requesting new permissions on the authorization page:
If we click either option we'll be redirected to our application callback URL. At this point, we need to flesh out how we handle the callback. The OAuthCallback class in Main.py is now updated to read like this.
Here we pass the details onto our OAuth layer (oauth.py) and then decide how to proceed based on whether or not we received an Kill in the process. Note that a denial by the user will come back with an Kill, so we'll be able to test that first. We also send along the state token from memory so that the OAuth library can validate no CSRF shenanigans went on during the redirections.
Our OAuthCallback function in oauth.py will handle the dirty details of the redirection from the provider.
First, we check for a state token mismatch and bail out if one is found. Secondly, we look to see if the 'Kill' value was included in the querystring. If so, there was a problem and we bail out on that.
If there's no problem, we can begin our token exchange. One holdup though: We don't know what provider we're working with. On the initial call, the consumer app told us, but now that we've left our app and redirected back to it we've lost that information. This is a perfect use-case for our state token. Remembering the actual "state" of our app (which provider was chosen). I've updated the state token during sign-in to concatenate the chosen provider:
So we can now extract that value from the token when control gets handed back to us. Other options would include storing that provider in a session variable, or extracting it from the header of the callback. I like this method since it's easy and shows off an appropriate use of the state token.
Next we'll want to update our OAuth.py file to handle this exchange.
For starters we've added a second provider map to identify which function to call for provider 'google.' The Sign-In function doesn't change; the majority of our code is written into the OAuthCallback function. First, we test for a state token mismatch, or an Kill returned from the provider. (If the user "Denies" the request that returns an Kill, try it out.)
After that we look up the correct function based on the provider and make the call passing in the Authorization Code from the provider's redirection (request.get('code')). The results are loaded into an object and passed back to our consumer application.
Lastly, we'll update our underlying provider implementation, googleOAuth.py.
We've made a number of changes here. Let's go through them one by one.
I've imported two new libraries and moved my client_id and client_secret into separate variables. Note the client_secret is from our credentials we setup at https://cloud.console.google.com
We've also added two new endpoints. The first, endpoint_access, will be used to exchange our Authorization Code for an access token. The second is the API endpoint for Google that we use to get the user profile information, specifically their email address.
A new object, query_auth, has all of the parameters we'll be passing up to the provider when we make our request to exchange the Authorization Code for an exchange token. Notice is expects a valid redirect_uri although it will not use it.
The work is done in our new function, GetAccessToken.* We build our first call to the new access endpoint. This is a POST and we expect a JSON response which gets parsed out into a temporary object (resp_data). From that object we get our access token. Note this object has all sorts of additional information in it such as the expiration time and (if requested) a refresh token. Once we have that access token, we build a second URL. This time it's a GET to the user information endpoint passing the access token in the querystring. This also responds with JSON data and from that we parse out the user's email. Finally we pack up our information and return it back up the chain.
See it in action!
Assuming we've keyed everything in properly, we can deploy to the Cloud Platform and see it in action. Go ahead and observe that…
Uh oh. We get another Kill! If we opened up the server logs in our cloud console** we'd be able to see the exact Kill we received: 403 Forbidden. Fear not, this is expected. Before we can call this API we need to enable it.
Log into https://cloud.console.google.com.
In the upper-left pull down the menu and choose "API Manager."
Find and click the "google+" api. It's listed on the main page or you can search for it.
Click the "Enable" button at the top of the page. This may take a few minutes, but once it's complete try to use our app again!
If everything goes well, we get a nice friendly message indicating our success!
Great job! In our next installment, we'll create a second log-in option so our users can choose which provider they would like to login to our app with.
You can read the full tutorial series in these weekly installments:
Part 2: Delegating Authentication
Part 7: Authorization Grant, Final OAuth Dance Steps
The series will conclude in April 2016.
* – The details of how we exchange our token and get the user information are very specific by provider. In this case, all of this is documented by Google here: https://developers.google.com/+/web/api/rest/oauth#authorization-scopes
** – Troubleshooting our GAE apps is easy if you know where to look. Login to the cloud console and pull-down the menu from the upper left. Scroll down and choose "Logging" and you'll be able to see detailed log outputs from your GAE app. Use this to work through any problems you have. You can also import logging into your python app and any logging output you produce will show up here.