The Nitty-Gritty of OAuth 2.0 Flow

Valerie Chapple
18 min readFeb 22, 2018

--

Use Google’s REST API to see the inner workings of handling the OAuth 2.0 flow and never be confused again.

Requirements

Goals

We’re going to build a web app, similar to this demo web page, that will walk us through the OAuth 2.0 handshake to get a user’s access_token. The code for this tutorial can be reached here.

In addition to learning how to implement OAuth 2.0, you’ll also learn to…

  • register a web app using Google Cloud Platform
  • use webapp2 routing, page templates, and sessions
  • use urlfetch to make server-side requests

You will NOT learn

  • how to use the Python library for Google Authentication
  • how to secure and store access tokens or user information in a data store

OAuth 2.0 Overview

The basic idea is for an app to ask a user if they want to sign in to give permissions (authorizations). Once the user approves through the Google or another service, the app will receive an authorization code. The app can use that authorization code to ask for access tokens, which in turn can be used to ask for information from the service provider about the user.

The OAuth 2.0 handshake involves the Authorization request and the access token request. The access token is the end goal because it allows the app to finally access the user’s information.

If only drawing a graphic was as easy as actually implementing OAuth 2.0… So here is an overview of the steps that we’ll work through in nitty-gritty detail.

  1. An app registers with Google, obtaining a client_id and a client_secret.
  2. An app provides a way for a user to signal they wish to sign in to Google.
  3. The app redirects the user to a Google URL to sign in, attaching the app's credentials and a random state variable, as well as other specifications, to the URL query string.
  4. The user chooses to sign in and grant permissions to the app.
  5. After the user grants permission, Google sends a response to the app at an agreed upon route (a callback route). The response contains an authentication code and the exact same state variable it received.
  6. The app checks that the state variable is the same state variable from step 3 before continuing with the OAuth flow.
  7. The app uses the authentication code from Google to request an access_token, using a POST request.
  8. If the authorization code is valid, Google will respond with an access_token (and possibly arefresh_token). Save these tokens for future use.
  9. The access_token is used to access the user’s data without requiring the user to sign in. That is, send the access_token in the Authorization header (such as a Bearer asdsadfhsdiwej191293djsd) to one of Google’s API endpoints. For example, an endpoint to get basic user information is https://www.googleapis.com/auth/userinfo.email. This step can be repeated for as long as the access_token is valid. Next step is to consider looking at the Google API Documentation on refresh_token.

Part 1 Register the App with Google

Sign In

Register your web app with Google Cloud Platform by navigating to https://console.cloud.google.com/ and signing in with your preferred Google account. If prompted, agree to their Terms of Service.

Create a Project

In the upper-left, click Select a project. If you already have projects, you’ll see them listed. To create a new project, select the + button.

Name your project and optionally adjust its unique project id.

Google Cloud Platform

If your project is not selected, click Select a project and click on your new project. You should now see a dashboard with information about your project.

Now, there are a lot of tools at your disposal, some free and some not. We will only be working with free tools for this demo.

In the menu bar on the left, we’ll use two services:

  • APIs & Services
  • App Engine.

If you don’t see this side menu, use the three bar icon in the upper-left to display it. Additionally, you may choose to pin these two services to the top of the menu for easy access.

Setup OAuth Consent

Users need to know to whom they are giving information.

  1. Select the OAuth consent screen tab.
  2. Make sure your email address is the one you wish to have associated with your project.
  3. Add a product name. Other information is optional.
  4. Click Save.

Setup Credentials

We’ll need to create credentials for this project with Google. The credentials will be used in your web app to make HTTP requests to Google.

Navigate to App Engine and select Credentials. Here you’ll see future credentials we make. If you have multiple web apps accessing the same project, you’ll use multiple credentials.

Click the button called Create credentials and select OAuth client ID. This will allow us to request information from our users, such as name and email.

Select the application type of Web application since we will be using a website and server to make calls to Google.

Create a name for this client ID, such as OAuth Demo or Medium Web App Demo.

One of the easiest mistakes to make and hardest to troubleshoot when working with APIs is to not correctly setup the redirect URIs. The redirect URIs must be EXACTLY the same as the one you send to Google with your authorization requests. If not, then the request is rejected. See below for some variations to consider when setting up the redirect URIs. Later you’ll need to add your deployed app’s redirect URIs.

Or even consider changing localhost to 127.0.0.1.

Click the Create button to register your app.

Save Credentials

You will need to have your client ID and your client secret to make requests to Google. Save these two values in a file for now. You’ll be able to download the information later if you need.

Part 2 Web App Structure

Create a skeleton web app.

We will be using Python 2.7 and Google App Engine to serve our users. We will need to create the following file structure.

app.yamlstatic/js/*.js
static/css/*.css
index.html
500.html
success.html
main.py
BaseHandler.py
OAuthHandler.py

/app.yaml

This file clearly explains how the app is to the code supplied and how to handle routes. While we need only one route to run our app, we are going to include a second route for us to include CSS and JS in our web pages. The first url, /static, will serve our static JavaScript and CSS. The second url will field all other routes using a wildcard .* . The script: main.application is related to our file main.py, which contains routes setup by webapp2.

runtime: python27
api_version: 1
threadsafe: false
handlers:
- url: /static
static_dir: static
- url: .*
script: main.application

The application script will support four routes. These will be referenced within the server code, as well as in the HTML files.

/oauth             # Home Page
/oauth/authorize # Create URL to send to Google for Authorization
/oauth/callback # Route to handle Google's authorization response
/oauth/token # Route to process the access token

/static/

This folder will contain two subfolders: js and css. Within each of those, you’ll upload Bootstrap JS and CSS files, jQuery files, and other JS and CSS that you will be referencing in the HTML files. The CSS styling of our app will prevent Google from thinking our website is phishing for user information.

All HTML files

We will create three HTML files, which we will use as templates on our server. You may choose to organize these files into a templates folder.

/index.html         # Used in the /oauth route
/success.html # Used in the /oauth/token route
/500.html # Used for errors during authentication

Within each HTML file, add the following headers to include the CSS and JS files for Bootstrap.

<!-- META TAGS-->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap & CSS -->
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
<link rel="stylesheet" href="/static/css/style.css" />
<!-- Bootstrap & jQuery (jQuery must be listed first) -->
<script src="/static/js/jquery-2.2.4.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>

Add a Bootstrap navigation bar to the top of the body of each HTML file (adjusting or removing the sr-only span as needed). An example is below.

<nav class="navbar navbar-toggleable-md navbar-light bg-faded"><!-- TOGGLE BUTTON FOR LINKS -->
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- MENU TITLE -->
<a class="navbar-brand" href="#">OAuth 2.0 Demo</a>
<!-- COLLAPSED LINKS -->
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<!-- LINK TO START OAuth PROCESS -->
<a class="nav-item nav-link active" href="/oauth">
Start <span class="sr-only">(current)</span>
</a>
<!-- LINK TO Logout OF GOOGLE ACCOUNTS and return to Start -->
<a class="nav-item nav-link" href="https://www.google.com/accounts/Logout?continue=https://appengine.google.com/_ah/logout?continue={{ url }}">
Google Sign Out
</a>
</div>
</nav>

For those unfamiliar with templating, notice the {{ url }} in the Google logout link above. This is the notation used to allow you to dynamically give values to the HTML files. We’ll discuss this more later.

Specific HTML Content

/500.html — For generic server errors, we’ll send a page based off of the 500.html file. Put the following contents in the HTML body but below the navbar.

<div class="container">
<div class="row">
<div class="col-lg">
<h1>500 - Server Error</h1>
<a href="/oauth">Return to Home</a>
</div>
</div>
</div>

/index.html — For the home page, we’ll send an explanation of the app and a button for users to click to begin the authentication process. Put the following contents in the html body but below the navbar.

<div class="container">
<div class="row">
<div class="col-lg">
<h1>OAuth 2.0 Demonstration</h1>
<p>
This app uses HTTP/REST to authenticate users. Once this app is authorized by a Google+ user, it will display the user's first and last name, along with a link to the user's Google+ profile page. Additionally, it will show the state variable used to authenticate the callback from Google.
</p>
<h1>Begin Authorization</h1>
<p>
Please click the button below to authorize this application using Google+. It will only request access to <code>email</code>.
</p>
<a class="btn-primary btn" href="/oauth/authorize">
Authorize App</a>
</div>
</div>
</div>

/success.html — This template is used to display the user information after authentication. We will use the access_token to get the user information and then send that information through the template. Put the following contents in the html body but below the navbar.

<div class="container">
<div class="row">
<div class="col-lg">
<h1>OAuth 2.0 - SUCCESS!!!</h1>
<p>
You have successfully authorized this application through Google+.
</p>
<ul>
<li>
<bold>Name:</bold> {{ name }}
{% if email %} (email: {{ email }}) {% endif %}
</li>
{% if link %}
<li><bold>Link:</bold> <a href="{{ link }}">{{ link }}</a></li>
{% else %}
<li><bold>Link:</bold> No Google Plus Account To Link To</li>
{% endif %}
<li><bold>State:</bold> {{ state }} </li>
</ul>
</div>
</div>
</div>

Notice there is more templating in this file. Variables are still enclosed with double {}; however, conditional statements are enclosed by {% and %}. For instance, {% if email %} will execute the next line of HTML code if there is no variable pass that is called email. The block of code continues to include an {% else %} and an {% endif %}. This logic helps use take care of nuances with data returned from Google.

/main.py: Web App Routing

The app.yaml will reference main.py for all routes not in the static folder. So we need to tell the program which route handlers to call for each route. We’ll also setup session configurations.

Imports — Import the following components to the main.py file.

import webapp2
from webapp2 import Route
from webapp2_extras import sessions

We will also import our request handlers from a file called OAuthHandler.py, which we will create later.

from OAuthHandler import OAuthHandler, AuthorizeHandler, CallbackHandler, TokenHandler

Sessions Configuration webapp2_extras allows us to create user sessions, which we will use to store the state variable we send to Google and Google sends back. Create a configuration object below the imports. This object will store the session key of your choosing. This object will be passed to our application instance once we create it.

# Session Configurations
configuration = {}
configuration['webapp2_extras.sessions'] = {
'secret_key': "secret_session_key",
}

Route Definitions —Below the initialization in main.py, define the following routes as we declare an instance of WSGIApplication. There are other ways to handle the difference between /oauth and /oauth/; however, this will work for our purposes.

# Routes
application = webapp2.WSGIApplication([
Route('/', handler=OAuthHandler, name='oauth'),
Route('/oauth', handler=OAuthHandler, name='oauth'),
Route('/oauth/', handler=OAuthHandler, name='oauth'),
Route('/oauth/authorize', handler=AuthorizeHandler, name='auth'),
Route('/oauth/callback', handler=CallbackHandler, name='cb'),
Route('/oauth/token', handler=TokenHandler, name='oauth-token'),
], debug=True, config=configuration)

/BaseHandler.py: Sessions

We’re going to create a request handler that supports sessions and from which all other handlers will inherit.

From the webapp2 documents

import webapp2
from webapp2 import RequestHandler
from webapp2_extras import sessions
class BaseHandler(RequestHandler): def dispatch(self):
# Get a session store for this request.
self.session_store = sessions.get_store(request=self.request)
try:
# Dispatch the request.
RequestHandler.dispatch(self)
finally:
# Save all sessions.
self.session_store.save_sessions(self.response)
@webapp2.cached_property
def session(self):
# Returns a session using the default cookie key
return self.session_store.get_session()

Notice that this handler gets a session from the sessions store and it also takes care of saving the sessions in the store. Now we will have a place to store our state variable.

/OAuthHandler.py

All four handlers for OAuth 2.0 will be implemented in this file.

Imports — Import the following into OAuthHandler.py.

from webapp2 import redirect  # reroute user to Google
import json # convert between JSON and strings
import os # Set the path for HTML files
import urllib # Used to encode data for URLs
import uuid # For unique random number for state
import hashlib # For hashing the random number
# Make server-side requests
from google.appengine.api import urlfetch
# Serve Dynamic HTML files to users
from google.appengine.ext.webapp import template
# Import our Basehandler class
from BaseHandler import BaseHandler

OAuthHandler Class — This class will simply serve the home page and pass the web app’s url to the template. As we progress, we’ll be able to change the variable from localhost:8080 to the actual URL of our deployed application.

URL = "http://localhost:8080"class OAuthHandler(BaseHandler):
def get(self):
path = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(path, {"url": URL}))

Notice that OAuthHandler inherits from BaseHandler so it will have easy access to the user session. It also sends index.html to the client the with the {{ url }} template variable defined.

Recall that within the index.html file there is an Authorization link (formatted to look like a button). This link directs the user to our next handler at the route /oauth/authorize, which is controlled by the AuthorizeHandler.

<a class="btn-primary btn" href="/oauth/authorize">Authorize App</a>

AuthorizeHandler Class — This class takes care of directing the user to Google. That is, this handler does not send content back to the user but is in charge of constructing the appropriate redirect URL to request authorization from Google.

  1. It creates a state variable and saves it to the session.
  2. It constructs parameters to send to Google’s OAuth endpoint
redirect_uri = URL + "/oauth/callback"
client_id = "YOUR CLIENT ID HERE"
class AuthorizeHandler(BaseHandler):
def get(self):
# Create & save state variable
num = uuid.uuid4().hex
hash_obj = hashlib.sha256( num.encode() )
state = hash_obj.hexdigest()
self.session['state'] = state
# Construct URL with appropriate parameters
url = "https://accounts.google.com/o/oauth2/auth?"
url += "scope=email"
url += "&redirect_uri=" + str(redirect_uri)
url += "&client_id=" + str(client_id)
url += "&response_type=code"
url += "&access_type=online"
url += "&include_granted_scores=true"
url += "&state=" + str(state)
url += "&prompt=select_account"
# Redirect user to URL as a GET request
self.redirect(url) # Redirect to Google

Note: the uuid and hashlib are used to create a unique state code. A time out feature could be added to ensure the state expires in a reasonable time.

The user is then redirected to the url that is constructed according to the Google OAuth API documents. Some important notes about the query parameters…

Required Parameters

  1. scope is representative of how much authorization you are asking of your user. Only ask for permissions you need and only as you need it. More scopes here.
  2. redirect_uri is the callback URL to our app that Google will send the response. For us, that is /oauth/callback. NOTE: We’ll need to register this redirect with our Google Credentials EXACTLY the same as in our URL.
  3. client_id contains our Google issued client ID that we saved earlier.
  4. response_type=code means that we are expecting an authorization code to be returned.

Other Parameters

  1. access_type can be online or offline
  2. include_granted_scopes can be true or false. true means that you retain previously granted permissions as you ask for more permissions.
  3. state is used to increase confidence that the incoming response from Google is valid.
  4. prompt informs Google whether to ask the user for consent and/or to have the user select_account every time OR only ask the first time.

After the user signs in through Google to grant the app permission, the user will be redirected to our callback URL. For us, that is /oauth/callback, which is handled by our CallbackHandler.

CallbackHandler Class — This class handles the/oauth/callback route and processes the response from Google after the user logs in. The Google response will either contain a query string with an error or a query string with the authorization code and state value.

  1. Check for and return to the user any error returned by Google.
  2. Verify the state variable stored in sessions is the same as the state variable redeemed in the query string. Send an error if incorrect.
  3. Use the authorization code in the query string to construct a URL to obtain an access_token from Google. This request to Google will be a POST request from our server to Google, which Google will respond with an access_token.
  4. Once the access_token is received by our app, the user is redirected to our /oauth/token route.

Note: This handler does not send content back to the user unless there is a server error, at which time it will send the 500.html template. Instead, the handler grabs information from the response and then redirects the user to a content page.

redirect_uri = URL + "/oauth/callback"
client_id = "YOUR CLIENT ID HERE"
client_secret = "YOUR CLIENT SECRET HERE"
class CallbackHandler(BaseHandler):
def get(self):
# Check For Error
err = self.request.get("error");
if (err != ""):
path = os.path.join(os.path.dirname(__file__), '500.html')
context = {}
context['url'] = URL
self.response.out.write(template.render(path, context))
return
# Check state from URL is valid
state = self.request.get("state");
if (state != self.session.get('state')):
self.response.write("Invalid State Request");
self.session['state'] = ""
return;
# Get authorization code from parameters
code = self.request.get("code");
# Create URL to get access_token in POST request
url = "https://accounts.google.com/o/oauth2/token"
try:
# Create POST Body and encode
data = {}
data['code'] = str(code)
data['client_id'] = str(client_id)
data['client_secret'] = str(client_secret)
data['redirect_uri'] = str(redirect_uri)
data['grant_type'] = "authorization_code"
edata = urllib.urlencode(data)
# Create Server-Side Request
headers =
{'Content-Type': 'application/x-www-form-urlencoded'}
res = urlfetch.fetch(
url=url,
payload=edata,
method=urlfetch.POST,
headers=headers )
except:
# Report server error
path = os.path.join(os.path.dirname(__file__), '500.html')
context = {}
context['url'] = URL
self.response.out.write(template.render(path, context))
return
# Process response to save access_token to session
payload = json.loads(res.content)
self.session['access_token'] = payload['access_token']
# Redirect User to token handler
self.redirect(URL + "/oauth/token");

This CallbackHandler handler is different than the AuthorizeHandler where the browser is redirected to Google. Instead, CallbackHandler makes a server-side POST request to Google and handles Google’s response without any input from the user.

The server-side request from above is made possible by encoding the POST body with urllib.urlencode() and using urlfetch.fetch() to initiate the request. The return value of urlfetch.fetch() is the response object. This response object has content, which hopefully contains the Bearer token called access_token. Save the access_token to the state and then redirect the user to a handler that will display information, such as to our route/oauth/token, which is handled by TokenHandler.

TokenHandler Class — This class takes care of requesting user data and displaying it on the webpage. That is, the server will make a third Google request to get the User’s name and Google+ profile link. However, this third request is not part of the OAuth2.0 handshake. Instead, it is an example of how to use the access_token to make requests of Google’s APIs.

  1. Create a GET request using encoded parameters and an Authorization header with the savedaccess_token and send to the Google+ API endpoint, https://www.googleapis.com/oauth2/v2/userinfo.
  2. Process response from Google to check for an error and to correctly obtain the user’s name and profile link.
  3. Pass the user’s name and profile link to the success.html template.
class OAuthTokenHandler(BaseHandler):
def get(self):
# Get parameter access_token
token = self.session.get('access_token')
try:
# Get User's JSON from google plus
url = "https://www.googleapis.com/oauth2/v2/userinfo"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
"Authorization": "Bearer %s" % token }
res = urlfetch.fetch(
url=url,
headers=headers )
except:
# Error fetching User info
self.response.write(res.content)
self.session['access_token'] = ""
self.session['token_type'] = ""
return
# Valid results of response
payload = json.loads(res.content)
# Deal with authentication error
if 'error' in payload:
path = os.path.join(os.path.dirname(__file__), '500.html')
context = {}
context['url'] = URL
self.response.out.write(template.render(path, context))
return
# Grab data for template page
context = {}
context['url'] = URL
try:
# Send Name to context
if 'name' in payload and payload['name'] != "":
context['name'] = payload['name']
else:
context['name'] = "No Name Provided"
context['email'] = payload['email']
# Send Link to context
if 'link' in payload:
context['link'] = payload['link']
# Send State to context
context['state'] = self.session.get('state')
except:
path = os.path.join(os.path.dirname(__file__), '500.html')
context = {}
context['url'] = URL
self.response.out.write(template.render(path, context))
return
# Send context to HTML
path = os.path.join(
os.path.dirname(__file__), 'success.html')
self.response.out.write(template.render(path, context))

Part 3 Test App Developmentally

To test app locally, you’ll need to have Google Cloud SDK installed so that your folder can appropriately access its tools. This tutorial was written using Python 2.7 so make sure that you have installed the SDK in the correct environment.

Within your app folder, run dev_appserver.py app.yaml. Once updates finish, you should see a line that says Starting admin server at: http://localhost:8000. This means that you’ll be able to navigate to your app.

Navigate to http://127.0.0.1:8080/ or http://localhost:8080 to view your demo, which should look similar to the image below.

Request Page for Authorization

If you click on Authorize App, you’ll be redirected to Google to give Consent to OAuth Demo (or whatever name you gave your app).

Consent screen

Once you sign in, you’ll see a screen like below. The screen will look slightly different if you used a Google account that is not registered with Google+.

Authorization complete

Part 4 Deploy App on Google App Engine

Now for the real deal… deployment. We have our files ready, and just need a few tweaks and uploads to deploy

Google Cloud Shell Navigate to Google Cloud in your web browser. Select the console icon to launch Google Cloud Shell.

Google Cloud Shell

Base Url The name of the console tab will be your web address for your app. That is, this example would be web-app-demo.appspot.com. Update the URL variable in OAuthHandler to be the deployed app’s URL.

Redirect URI Now that we know the URL of our deployed app, we can add that URL to our list of authorized redirect URIs with Google. You’ll want to add to the authorized list the same URL paths listed for the localhost, but replace localhost with your app’s base URL. (Reference the end of Part 1.)

Upload Files Near the top of the Google Cloud Shell are icons. Select the vertical three dots icon to display a menu. Select Upload file.

Select all the files you created in this tutorial. Create folders as needed within the Google Cloud Shell. You may also choose to organize all these files within a folder to isolate from other future versions.

Deploy App Once all your files are organized in the same fashion as you have locally, you can deploy your app. Type gcloud app deploy app.yaml. You’ll be walked through the setup. Once done, you can visit your app through the url provided at the end.

What now?

I personally enjoyed working with the OAuth Playground and the various APIs offered by Google. It provided an easy way to examine requests and responses with Google’s APIs.

There are so many things to explore once you have the OAuth 2.0 handshake setup. First of all, there is more than the Google+ API. If you use Google Drive, explore how to find, create, and move files, or examine how to make events with Google Calendar API.

And take a moment to explore other services that require authorization, such as Twitter or Facebook sign in or the Github API.

Lastly, I recommend using third-party libraries to streamline authorization. We can then use our knowledge about OAuth 2.0 to understand and troubleshoot issues we may have with the library.

--

--