Using decorators to require sign in with bottle.py
In the last few weeks I have had a chance to experiment (and fall in love with) bottle.py—a super-compact, python-based framework for web apps. The API docs are solid and the community has been extraordinarily helpful.
Like most hackers, I always try to teach myself something before asking for help. And for a long time (way too long in fact) I struggled to understand how function decorators could be applied to enforce user authentication on various routes within bottle. Obviously, certain user specific content should only be shown to the user who created it.
In the bottle list, Greg Stein shares a key piece of information:
In my application, I have a decorator that I apply to each route that requires authorization. Something like this: @route(‘/main’, view=’main.ezt’)
Gregg uses the same mechanics for requiring SSL as he does user authentication. Smart. For now, I just needed the @require_reg bit to work.
First, I needed to get more comfortable with python’s decorators. For that, see Kevin Samuel’s super kick-ass write-up on stack overflow. Here’s the structure of what I got to work. I’ll break it down in a bit:
def require_uid(fn): def check_uid(**kwargs): cookie_uid = request.get_cookie('cookieName', secret='cookieSignature') if cookie_uid: //do stuff with a user object return fn(**kwargs) else: redirect("/loginagain") return check_uid ... @route('/userstuff', method='GET') @require_uid @view('app') def app_userstuff(): //doing things is what i like to do return dict(foo="bar")
Alright, time to tear it down.
The goal of
require_uid is to make sure that anyone accessing a certain page has a valid user id set in a cookie. Because
require_uid ultimately decorates
app_userstuff or any other route function, it takes a function,
fn, as its argument.
require_uid I define a new function,
check_uid does the grunt work. For starters, it uses
**kwargs to accept a variable number of keyword arguments. Why? Think about routes. Some of could be example.com/page/26 or example.com/blog/2011/10/31. In the former, the route function is probably something like:
@route('/page/:pageNum', method='GET') @require_uid def page_func(pageNum=1): //do stuff with pageNum return dict(templateData=pageFuncData)
And, the latter example would be something like:
@route('/blog/:year/:month/:day', method='GET') @require_uid def blog_func(year=2000, month=1, day=1): //do stuff with year, month, day return dict(templateData=blogFuncData)
As shown, the ‘require_uid` decorator gets applied to route functions that have a variable number of keyword arguments. I’ll come back to this in a sec.
require_uid uses bottle’s handy
request.get_cookie to grab a cookie named cookieName. (In this example, the cookie is signed by cookieSignature. As always, if you don’t sign your cookies, carefully think about why not. It’s an easy, basic step towards a safer app.) Here the cookie named cookieName should contain a user id number so I attempt to set
cookie_uid has a value (as in, a cookie named cookieName was found and was signed by cookieSignature),
check_uid does some stuff to a user object (for example, load the user from a database and set some variables like name and email address) then returns the function passed to
require_uid along with that functions keyword argument parameters. If
cookie_uid is null, the user gets redirect to /loginagain which could contain a login form.
require_uid finishes by returning the internally defined
check_uid — the magic behind python’s function decorators.
Later in the initial example code I have
@route('/userstuff', method='GET') @require_uid @view('app') def app_userstuff():
Here the route /userstuff has the decorator
require_uid applied. It is placed after the route definition and before the referenced view template (if used). I need to know what route to apply
require_uid to but I do not want bottle to even attempt assembling the template if the user is not signed in.
And, that’s it! Now my bottle app will make sure users are signed in before letting them access content within certain routes. More importantly, it uses some core python functionality to achieve the desired result.