OAuth and Web2Py Part 1
June 21, 2010 – 8:36 amMany Website integrations done today involves Authorization and Authentication via an open protocol known as RFC-5849 as a method for clients to gain access to server resources for a resource owner. In this nomenclature the web2py application is a client or as it was called in the original OAuth specification a consumer (of the resource.) the visitor to the application is the resource owner or User, and the remote connection that provides the resource is the service provider or server.
OAuth uses a three part handshake to verify the credentials of a connection. If there is an application that wishes to make a transactional request for an user with a 3rd party, the application and the 3rd party (Google, twitter, etc…) first communicate together setting up a public key encryption involving a key and a secret. (Called the client credentials) Using these credentials the application requests a temporary set of credentials called the Request Token that identifies a transaction to take place in the future. The application then redirects the end user to the server giving the request token as an argument to the URL. The end user identifies themselves to the service and on authentication the end user is redirected back to the application website to either a predetermined callback page or to a page specified in the redirect. At this point the client presents it’s oauth token and secret to the application which the application then use to connect to the remote server and validate it. This last step returns an access token. Until the access token expires, it may be cached to link an user with the permitted resources.
By design neither the application developer personally nor the application itself knows anything about the credentials that the user submitted to the remote system, the application does not know any passwords, nor does the application knows any e-mail addresses or other mechanisms to identify the user, it only knows that given this access token the remote server will respond with requested information about a specific user; to the remote system he has been authenticated, and the application has been authorized to make requests.
Acquiring OAuth:
You can get a Python OAuth library straight from its source and install it as a site-package from GitHub It also relies on httplib2 which can be acquired from Httlipb2‘s website. You can simply copy the python2/httplib2 and oauth2 into the web2py/site-packages directory.
For example application I will be creating a twitter client, it’s not the most original thing ever, but it’s a functional test. First register with Twitter’s OAuth Clients On this form, select a browser application, Read & Write access, that your app does want to use twitter for authentication, and specify a callback URL as site/app/controller/function. In the example I am using http://127.0.0.1:8000/twitter/default/callback Then a page will be displayed that shows the consumer key and secret, as well as the three URLs to connect to the server, the Request token URL, Access token URL, and Authorize URL. These are common with any OAuth service that I’ve seen.
Creating The Application
Create a new app through the administrative control panel and name it twitter. Begin editing models/db.py by appending to the end of the file:
consumer_key = "PASTED FROM TWITTER" consumer_secret = "PASTED FROM TWITTER" request_token_url = 'https://twitter.com/oauth/request_token' access_token_url = 'https://twitter.com/oauth/access_token' authorize_url = 'https://twitter.com/oauth/authorize'
Now that the global read-only data is in place, edit the controllers/default.py file to load the needed libraries:
from oauth2 import Client, Consumer, Token
try:
from urlparse import parse_qs, parse_qsl
except ImportError:
from cgi import parse_qs, parse_qsl
import time
import gluon.contrib.simplejson
The parse_sql trick is straight from the OAuth API. urlparse was added to the standard lib with Python 2.5 (And in Python 3.0 it has been moved to httplib.Parse) in Python 2.4 cgi is a base module so the above import will maintain backwards compatibility as is the way of web2py.
OAuth Tickets
There are three states that an user can be in when it comes to an OAuth connection, they can have no tokens at all in which case a Request Token will need to be generated and handed to them, they could be giving the application an oauth token for it to validate, or they could be fully authenticated and authorized and have a valid auth token associated with them. To tackle the states one at a time:
in the default.py controller file, edit the index method as:
def index():
consumer = Consumer(key=consumer_key, secret=consumer_secret)
client = Client(consumer)
resp, content = client.request(request_token_url, "GET")
if resp['status'] != '200':
redirect(URL(r=request, f='maintenance'))
# Turn response into dict
request_token = dict(parse_qsl(content))
# Store it in a session
session.request_token = request_token
redirect_location = "%s?oauth_token=%s" % (authorize_url, request_token['oauth_token'])
redirect(redirect_location)
A Note on Redirects
Web2py redirect() method acts as a Python exception, all code stops being executed at that time and a 302 HTTP redirect header is sent to the client. From web2py’s point of view, when the user is redirected a new request and response object is created as it is an entirely new connection. This enables the application to control the work-flow in a dynamic fashion.
What I’ve done here is to create a consumer object using the consumer key, then requested a request token for this transaction. If anything went wrong with this request I’ve redirected the user to a maintenance page. Without a twitter token and with no way to get one the application doesn’t do anything. However if one was retrieved properly (As it probably would be in almost every case) it is then converted to a dict and store it in the current session for later retrieval. In the future it will be stored in the auth database instead. What is being stored in the session? Just two pieces of data:
oauth_token : tPmOLbed75wgvqVKZXmStShhNIBSmaGBjIOBABGHHo
oauth_token_secret : THE SECRET REMOVED
Now access the page with a web browser, You will be asked to log in via a twitter log in page and then you will be redirected to a page that doesn’t yet exist, in my case: http://127.0.0.1:8000/twitter/default/callback?oauth_token=UkGtfUfvQ4rJlNIn35WhHi8UBNXo9EPVMuzvOvcavM where the later part was the oauth token returned from the log in. This oauth_token now needs to be validated with the remote server and turned into an access token to complete the third part of the handshake. Doing this requires the callback function to be implemented:
def callback():
if session.request_token:
consumer = Consumer(key=consumer_key, secret=consumer_secret)
token = Token(session.request_token['oauth_token'],
session.request_token['oauth_token_secret'])
client = Client(consumer, token)
resp, content = client.request(access_token_url, "GET")
if resp['status'] != '200':
redirect(URL(r=request, f='maintenance'))
access_token = dict(parse_qsl(content))
session.access_token = access_token
redirect(URL(r=request, f='index'))
At this point there is now a successful access_token in the session and calls can be made using it. Each OAuth server can return additional data about the session with the access_token. However, this is not required and is completely specific to the server being connected. For example, Twitter will return two pieces of data: session.access_token['screen_name'] is the screen name of the logged in person, and session.access_token['user_id'] is the user id. Now that the access_token has been created, new requests can be made to gather more data. Add a new block to the start of index() as so:
if session.access_token:
consumer = Consumer(key=consumer_key, secret=consumer_secret)
token = Token( session.access_token['oauth_token'], session.access_token['oauth_token_secret'])
client = Client(consumer, token )
resp, content = client.request('http://api.twitter.com/1/statuses/home_timeline.json','GET')
if resp['status'] != '200':
if resp['status'] == '401':
session.clear()
redirect(URL(r=request, f='index'))
redirect(URL(r=request, f='maintenance'))
return dict(statuses=gluon.contrib.simplejson.loads(content))
This block of code is something that will be seen repeatedly in dealing with OAuth requests. Create a consumer, Create a token, Create a client, make a request of the client, verify the result of the request, decompress the request into a dict. Additional checks to ensure that the token is still valid. If it’s not then all data associated with the session will be cleared and the login workflow will begin again by redirecting the user back to the index page. Here a request was made for JSON encoded data, one could also ask for XML encoded data from Twitter and parse it with an XML reader. To my knowledge there is no difference among the two methods of encoding. I prefer to request JSON just in case I need to display the raw data on the client and manipulate it via JavaScript.
By the end of the controller’s index() execution, any status updates that the user would see from the front page of Twitter is loaded into the statuses key and can be displayed in the view. To do so, edit views/default/index.html as:
{{if statuses:}}
{{tab = TABLE(TR(TH('Tweeter'),TH('Tweet')))}}
{{for status in statuses: }}
{{tab.append(TR(TD(status['user']['screen_name']), TD(status['text'])))}}
{{pass}}
{{=tab}}
{{pass}}
This ends the initial post on how to use OAuth with Web2Py, in the next post I will discuss integrating the web2py auth by associating an existing user with an oauth session, and finally with using oauth completely in place of the auth logins. I will also discuss decorators slightly and global functions to make your life easier.
No related posts.
03-14-10 12:26PM
4.68 mi in 209:40 (30:00 min/mi)
HR 127 bpm
[...] This post was mentioned on Twitter by Bruno Cezar Rocha, Doug Warren. Doug Warren said: #OAuth and #web2py tutorial using #twitter as an example http://dougwarren.org/2010/06/oauth-and-web2py-part-1/ [...]
Hi, I’m trying it in local.
There’s something that I’m doing wrong, but I don’t know what.
At twitter app, I configured all as urls: http://127.0.0.1:8000
At the moment, when I enter the app, it redirects me to twitter auth, when I accept, it’s always on a loop answering me if I accept or cancel the auth.
I test it in a shell, and session.* doesn’t have request_token and access_token.
I’m lost! I need some light!
Sorry my english.
Thanks for the article.
The tutorial has you specifying a separate page for the callback ‘http://127.0.0.1:8000/twitter/default/callback’ Now you don’t actually need to do so, but if you do not, then you will need to move the logic that is in the callback() function into the index() In fact, when I originally wrote this up, I had it as such. I moved it to it’s own callback to be more clear what was going on. Place everything in callback() other than the redirect at the start of index() and everything should work.
It worked.
Just target the callback url to:
‘http://127.0.0.1:8000/twitter/default/callback’
Also, I imported Token, it wasn’t imported.
– from oauth2 import Client, Consumer
++ from oauth2 import Client, Consumer, Token
Thanks Doug