Hi all,
I'm in the finishing stages of writing a cookie-based authentication
handler for CouchDB in Erlang. This is primarily going to be useful
for CouchApps (apps running purely in CouchDB), but this also touches
on a generic way to authenticate users via a CouchDB database, which
could be adopted by the current default HTTP Basic auth handler.
I've put the code up here: http://github.com/jasondavies/couchdb/tree/master
---
Here are the set of requirements that I'm trying to fulfil:
- No server-side session state. By this, I mean the server doesn't
know who is actually logged in or not, in contrast with traditional
session-based login systems e.g. Django where each session is a row in
the database. This is so that a cluster of CouchDB instances can be
easily used without having to keep them tightly in sync (i.e. you can
log in via one instance and you instantaneously are authenticated on
all other instances with no delay). This is actually quite tricky to
fulfil, as you can't really rely on nonces etc. for increased security
- No password information in the cookie, even in the hash. This just
makes it impossible for the password to ever be leaked, even if
<insert hash algorithm here> is cracked.
- Tamper-proof cookies. This is fairly obvious, use a secure hash
algorithm with a secret server-side key to prevent tampering.
- Retrieve users and their roles from a CouchDB database. This part
might be better separated out so it can be used by the default (HTTP
Basic auth) handler too. For now I'm implementing it as part of this
cookie-based auth handler.
---
My solution fulfils these as follows:
- The cookie is of the form
base64_urlsafe(username:expiry:SHA1(username:expiry:secret_key
+user_salt)).
- The expiry date in the auth token is an additional layer of security
- basically it is set to be something like 1 hour ahead of the current
time so that inactive tokens always expire at some point. For
example, an attacker may gain access to a user's machine and lift the
cookie auth token from the browser cache. This tries to limit this
kind of attack (of course it's quite difficult to defend against an
attacker having physical access). A new cookie is sent (with a new
expiry date) on every request. For performance reasons I have added a
+/- 0.1 hour window (10% of the timeout) so that new auth tokens are
only generated every 6 minutes.
- When CouchDB sees a valid auth cookie (i.e. hash and expiry date are
okay) then it will retrieve the specified user from the view and set
the roles appropriately for user_ctx. This means that if an admin
changes a user's roles, the effects are almost immediate depending on
the time it takes for a cluster to synchronise such changes.
- Retrieving user auth info (i.e. roles and password): This is done
using a special view "users" in a special design doc called "_design/
_auth". This view should return a structure with "roles" and
"password" members, with an optional "salt" member to be used as a per-
user salt.
- The secret key is stored in the special "_design/_auth" design doc
so that it can be easily shared by a cluster.
- The per-user salt is a useful way of being able to log out all
previous sessions for a user, simply by changing this to a new random
value.
- The cookie_authentication_handler can take an optional DbName
parameter, so that you can set a particular database to be the user
database for an entire CouchDB node.
---
Usage:
Edit your .ini files as follows:
1. Under [httpd] change authentication_handler to {couch_httpd,
cookie_authentication_handler} or {couch_httpd,
cookie_authentication_handler, "insert_userdb_here"}
2. Under [httpd_db_handlers] put:
_login = {couch_httpd_misc_handlers, handle_login_req}
_logout = {couch_httpd_misc_handlers, handle_logout_req}
or:
_login = {couch_httpd_misc_handlers, handle_login_req,
"insert_userdb_here"}
_logout = {couch_httpd_misc_handlers, handle_logout_req,
"insert_userdb_here"}
---
Still to do:
- Use some kind of challenge/response mechanism for logging in via
AJAX. At the moment the login handler just takes a plaintext username/
password combination sent via POST. I was thinking of using SRP (http://en.wikipedia.org/wiki/Secure_remote_password_protocol
), however I believe this would require state to be stored on the
server, and maybe isn't appropriate for this.
- Store hashes of passwords in the database. We can already do this,
but we might want to send something like hash(password+password_salt)
to the server, which would involve retrieving the appropriate
password_salt for a given user first.
- At the moment the cookie is set for Path=/ - this probably needs to
be set to Path=/current_database by default, and be configurable so
that it can be used by a proxy.
- I need to work on making my tests more exhaustive, they're pretty
minimal for the moment.
- All this auth stuff should probably go into its own module,
couch_httpd_auth or similar.
Any comments would be much appreciated.
Thanks,
--
Jason Davies
www.jasondavies.com