The One True Auth Server

Spoiler Alert: There isn't one


auth = authorization/authentication

  • authentication = you are who you say you are
  • authorization = you have permission to do something

Let's try to implement the simplest possible solution for this problem.

We'll use group membership as the mechanism for access. If you're in a group, you get access to those operations. If you're not in the group, you don't. Simple.

  • /login: Authentication gives you a unique token
    • /login/<email>/<password> -
    • /login/<basic_auth> - basically the above but base64 MIME encoded. Used by HTTP Basic Authentication
  • /valid(ate): Authorization tells you whether a user is a member of a group
    • /valid/<token>/<group> - is token a member of group?

interesting stuff:

  • hardcoded logins: this is how a lot of projects starts out.

  • anonymous logins: this allows us to make users without a lot of fuss.

  • email logins: now we can use email as a way to authenticate users. Of course, we need a fully functional email system attached to do this.

  • social logins: with this, we can piggyback off of users' social logins.


Base Framework:

Python/Bottle

How do we store this stuff? In memory is probably the fastest/easiest but it's not persistent.
Hack: Persist records on every Create/Update/Delete, and read in all records when you start up. Sort of a poor mans' write-through cache.

Hardcoded:

#!/usr/bin/env python                                                                                                                          
import bottle,json,base64  
app=bottle.Bottle()

# our in-memory db                                                                                                                             
app.UserList=[  
  { "e":"j@x", "u":"u0", "t":"t1", "p": "a", "g": ["user","admin"] },
  { "e":"z@w", "u":"u1", "t":"t2", "p": "b", "g": ["admin","root"] },
  { "e":"q@q", "u":"u2", "t":"t3", "p": "c", "g": [], "disabled": True, }
]

def digest(u,p): return base64.b64encode(u+':'+p)

# indexes                                                                                                                                      
app.L = dict( (digest(u['e'],u['p']),u) for u in app.UserList )  
app.T = dict( (u['t'],               u) for u in app.UserList )

# util funcs for error conditions                                                                                                              
def AccessDenied():  
  raise bottle.HTTPResponse(status=403, body=json.dumps(dict(success=False,
                                       reason="Access Denied")))

@app.hook('after_request')
def enable_cors():  
    bottle.response.headers['Access-Control-Allow-Origin'] = '*'
    bottle.response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
    bottle.response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'

@app.get('/valid/<token>/<group>')
def is_valid(token,group=None):  
  value = app.T.get(token,None)
  if not value:  # you're not in the DB!                                                                                                       
    return AccessDenied()
  if value.get('disabled',0): # you're disabled!                                                                                               
    return AccessDenied()
  if group and group not in value['g']: # are you in the group?                                                                                
    return AccessDenied()
  return dict(result=True) # all tests pass, you're in                                                                                         

@app.get('/login/<username>/<password>')
@app.get('/login/<username_password>')
def login(username_password=None,username=None,password=None):  
  if username_password is None:
    username_password = digest(username,password)
    pass
  value = dict( app.L.get(username_password,{}) )
  if not value:  # you're not in the DB!                                                                                                       
    return AccessDenied()
  value.pop('p',0) # don't need to be sending the password around                                                                              
  value.pop('g',0) # or the groups                                                                                                             
  return dict(result=dict(authinfo=value))

if __name__=='__main__':  
  app.run(host='', port='9090')

So we got where we need to go in 40 lines of python (whitespace/comments not included).

Here's there gist of it over on github.

Things missing that would be nice to have:

  • No way to add/edit/delete users except by changing the file and rebooting
  • No way to allow logins from social
  • A nice console