The Nitty-Gritty of OAuth 2.0 Flow

Valerie Chapple
Feb 22, 2018 · 18 min read

Requirements

Goals

You will NOT learn


OAuth 2.0 Overview


Part 1 Register the App with Google

Sign In

Create a Project

Google Cloud Platform

Setup OAuth Consent

Setup Credentials

Save Credentials


Part 2 Web App Structure

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

/app.yaml

runtime: python27
api_version: 1
threadsafe: false
handlers:
- url: /static
static_dir: static
- url: .*
script: main.application
/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/


All HTML files

/index.html         # Used in the /oauth route
/success.html # Used in the /oauth/token route
/500.html # Used for errors during authentication
<!-- 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>
<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>

Specific HTML Content

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

/main.py: Web App Routing

import webapp2
from webapp2 import Route
from webapp2_extras import sessions
from OAuthHandler import OAuthHandler, AuthorizeHandler, CallbackHandler, TokenHandler
# Session Configurations
configuration = {}
configuration['webapp2_extras.sessions'] = {
'secret_key': "secret_session_key",
}
# 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

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

/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
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}))
<a class="btn-primary btn" href="/oauth/authorize">Authorize App</a>
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
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");
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

Request Page for Authorization
Consent screen
Authorization complete

Part 4 Deploy App on Google App Engine

Google Cloud Shell

What now?

Valerie Chapple

Written by

Software Developer, former Physics Teacher

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade