Using decorators to require sign in with

In the last few weeks I have had a chance to experiment (and fall in love with)—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’)
def page_main():
//do stuff

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)

    return check_uid


@route('/userstuff', method='GET')
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.

Inside require_uid I define a new function, check_uid. 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 or In the former, the route function is probably something like:

@route('/page/:pageNum', method='GET')
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')
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.

Next, 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.

If 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')
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.

  1. dbra reblogged this from kurttheviking
  2. kurttheviking posted this
Hacker & Craftsman