What can we do however if we want to be a bit more selective? For example, assume that you have built a small tool that you would like to share only with family and friends. How could you prevent other, unauthorized people, to just gain access to your app?
One solution to the problem is store a list of permitted users in a list (either hardcoded or in a database). This will work if you know exactly who the selected few are. but is also means that you have to administer the list and keep it up to date. A more generic system would be an invitation based access like gmail originally had, but that would mean one would also need to manage those invitations somehow in the database. The following example shows a middle ground -- a simple technology called HMAC to make sure a particular google account is actually supposed to have access.
The following code creates a cryptographic hash for given username. It uses a convenience-method from the cryptutil module that is part of the OpenID sample code. In order to do so, the app uses a secret key that is never revealed externally. Only the generated hash will be handed out to the end user:
import base64
from openid import cryptutil
def sign(username):
# In real life, choose a better secret!
secret='superSecretKey'
return base64.encodestring(
cryptutil.hmacSha1(secret, username))
So how does the validation work in practice? Let's look at a very simple request handler that makes use of it:
import cgi
import os
import urllib
import wsgiref.handlers
from google.appengine.api import users
from google.appengine.ext import webapp
class MainPage(webapp.RequestHandler):
def isValid(self):
"""Determines if a given request is valid.
To fulfill the criteria, a user has to be logged in
and either be administrator or pass along a signature
parameter that matches the given username.
"""
user = users.get_current_user()
if not user:
return False
if not users.is_current_user_admin():
expected_signature = sign(user.email().lower())
given_signature = self.request.get('signature')
if not given_signature == expected_signature:
return False
return True
Our validation method checks whether a given request has a signature-parameter. If it does, it will compare this with the hash computed from the current user's email address. Only if these two keys match will it consider the user to have access.
The following get and post methods build a very simple application that displays an important message to invited users only. By entering another user's email address, an invited user can have the system create a signed access url that he or she can pass along to another, authorized user:
def get(self):
"""Display a message for invited people only."""
# Require a login
user = users.get_current_user()
if not user:
self.redirect(users.create_login_url(self.request.uri))
return
# If user is not admin, require a valid signature
if not self.isValid():
self.error(403)
return
# Ok, the user is allowed to see this page!
# In a real app, this is where we would
# render a template ;-)
self.response.out.write("""
<html><body>
<h1>Hello, %s</h1>
<p>The secret message you have been waiting for:
<b>APP ENGINE ROCKS!!!</b>
</p>
<form method="POST">
Pass on the message to
<input name="invitee"/>
<input type="submit"/></form>
<p><a href="%s">Log out</a></p>
</body></html
""" % (cgi.escape(user.nickname()),
users.create_logout_url(self.request.uri)))
def post(self):
"""Create a URL to pass the message on."""
# If user is not admin, require a valid signature
if not self.isValid():
self.error(403)
return
# Get the name of the invitee
invitee = self.request.get('invitee')
if not invitee:
invitee = 'anonymnous'
invitee = invitee.lower()
# create the signature and return the url
signature = sign(invitee)
self.response.out.write("""
<html><body>
The invite-url is
http://%s?signature=%s</body></html>""" % (
os.environ['SERVER_NAME'],
urllib.quote(signature, '')))
What might seem like a toy on first sight is actually a simplified example of what has many practical applications in todays web apps: the question on how to verify that a particular web request came from a trusted source. For a more complete algorithm on how to make almost any get or post secure, check out the OAuth specification, which explains a simple generic algorithm on how to sign web requests using HMAC. App Engine contains all the building blocks for utilizing this technique and using it to making ones products more secure. So, please do :-)
2 comments:
So this is a nice little trick but I fail to believe its so much better. You still need to build into the system some way to invite or otherwise generate keys for users you want to allow. So, you still have manage the whitelist, in that you have to add people to it.
However, you remove your ability to take people back off that whitelist, by using this method over maintaining a list of authorized users. So long as you'd still have some part of the system to generate the key, you could have the same part add the user to a list. The check would be simpler and the addition of a "remove user" feature would be very small.
I guess I like the trick, but I don't see the benefit, yeah?
I probably oversimplified the example. You are right that, for permanent links, the lack of being able to revoke access is not great. In a more complete example, these kinds of links have a time sensitive component, such as an expiration date (to give someone temporary access to a resource), and that date is used as part of the string that the hash is generated from. But that makes the code a bit more complex, so I definitely sacrificed clarity over simplicity here.
Another use of HMAC is to actually be a substitute for the user login in the first place. The hash, together with a set of other parameters it was created from, serves as an access token that verifies that a certain request is valid, no matter who is logged in.
Either way, the server-side secret has to be changeable by the application developer (and should probably be changed on a regular base). So you are right, building "permanent" links this way that are handed out to many people is not an ideal use case.
Post a Comment