Implementing User Authentication with bcrypt in ChicagoBoss

Ever since I learned the error in using basic MD5/SHA1/SHA256/etc. hashes for storing password hashes, I always see to adding in bcrypt hashing in the authentication for my web applications, but ChicagoBoss, one of my new go-to web frameworks along with Revel (yay, concurrency!), doesn't have bcrypt support added in by default. Let's go ahead and add that ourselves.

Before we get into things, I just want you to be aware that this is a very basic implementation. There are many things I plan on changing (I may end up updating the gist as well), so please follow suit. Use this as a starting point, and adapt this to the needs of your project.

Initial Configuration

Let's add bcrypt to our rebar.config as a dependency:

{deps, [
    {boss, ".*", {git, "git://github.com/evanmiller/ChicagoBoss.git", "HEAD"}},
    {bcrypt, ".*", {git, "https://github.com/opscode/erlang-bcrypt.git", "HEAD"}}
]}.
{plugin_dir, ["priv/rebar"]}.
{plugins, [boss_plugin]}.
{eunit_compile_opts, [{src_dirs, ["src/test"]}]}.
{lib_dirs, ["./deps/elixir/lib"]}.

More than likely, you'll already have most of this except for line 3. To grab the source and compile bcrypt, run ./rebar get-deps compile.

Don't forget to configure a persistent data store for your user accounts in boss.config. This should work with the default mock db_adapter, but you will lose all data once you stop/restart the application.

Loading bcrypt

We need bcrypt's application to be running before we can use it. Sadly, I have yet to figure out the magic sauce to have ChicagoBoss run bcrypt automatically, so in the mean time, we'll use an init script to help us out:

%% file: priv/init/module_10_bcrypt.erl
-module(module_10_bcrypt).
-export([init/0, stop/0]).

%% We need to manually start the bcrypt application.
%% @TODO: figure out how to get this to run via boss.config.
init() ->
    %% Uncomment the following line if your CB app doesn't start crypto on its own
    % crypto:start(),
    bcrypt:start().

stop() ->
    bcrypt:stop().
    %% Comment the above and uncomment the following lines
    %% if your CB app doesn't start crypto on its own
    % bcrypt:stop(),
    % crypto:stop().

All modules with an exported init/0 in ./priv/init are loaded and called at initial application start. This is helpful for adding watches with boss_news as well.

Our User Model

Here's a basic user model for our account information with a few convenience functions sprinkled in:

%% file: src/model/test_user.erl
-module(test_user, [Id, Email, Username, Password]).
-compile(export_all).

-define(SETEC_ASTRONOMY, "Too many secrets").

session_identifier() ->
    mochihex:to_hex(erlang:md5(?SETEC_ASTRONOMY ++ Id)).

check_password(PasswordAttempt) ->
    StoredPassword = erlang:binary_to_list(Password),
    user_lib:compare_password(PasswordAttempt, StoredPassword).

set_login_cookies() ->
    [ mochiweb_cookies:cookie("user_id", erlang:md5(Id), [{path, "/"}]),
      mochiweb_cookies:cookie("session_id", session_identifier(), [{path, "/"}]) ].

Set an actual secret for your SETEC_ASTRONOMY like I will be.

This model contains one of the items I want to improve upon in the future. Eventually, the session storage will be moved over to Riak as its bitcask storage backend supports automatic expiry of keys, so I don't have to worry about invalidating old sessions as they expire. Chalk that up as being a lazy (smart) programmer.

A Helper Module

This helper module isn't really necessary, but it does provide a simple place to keep functions that don't really belong in our model. In fact, I see some refactoring that is in order to clean up the model and controllers even further.

%% file: src/lib/user_lib.erl
-module(user_lib).
-compile(export_all).

%% On success, returns {ok, Hash}.
hash_password(Password)->
    {ok, Salt} = bcrypt:gen_salt(),
    bcrypt:hashpw(Password, Salt).

%% Tests for presence and validity of session.
%% Forces login on failure.
require_login(Req) ->
    case Req:cookie("user_id") of
        undefined -> {redirect, "/user/login"};
        Id ->
            case boss_db:find(Id) of
                undefined -> {redirect, "/user/login"};
                TestUser ->
                    case TestUser:session_identifier() =:= Req:cookie("session_id") of
                        false -> {redirect, "/user/login"};
                        true -> {ok, TestUser}
                    end
            end
     end.

compare_password(PasswordAttempt, Password) ->
    {ok, Password} =:= bcrypt:hashpw(PasswordAttempt, Password).

user_lib:require_login/1 checks for the presence of session data and validates it, redirecting the request to our login page. If everything is good to go, it returns our TestUser.

Our User Controller

This allows our users to register for an account or login. It might be nice to let the logout in the future.

%% file: src/controller/test_user_controller.erl
-module(test_user_controller, [Req]).
-compile(export_all).

login('GET', []) ->
    {ok, [{redirect, Req:header(referer)}]};

login('POST', []) ->
    Username = Req:post_param("username"),
    case boss_db:find(annie_user, [{username, Username}], [{limit, 1}]) of
        [TestUser] ->
            case TestUser:check_password(Req:post_param("password")) of
                true ->
                   {redirect, proplists:get_value("redirect",
                       Req:post_params(), "/"), TestUser:set_login_cookies()};
                false ->
                    {ok, [{error, "Password mismatch"}]}
            end;
        [] ->
            {ok, [{error, "User not found"}]}
    end.

register('GET', []) ->
    {ok, []};

register('POST', []) ->
    Email = Req:post_param("email"),
    Username = Req:post_param("username"),
    {ok, Password} = user_lib:hash_password(Req:post_param("password")),
    TestUser = test_user:new(id, Email, Username, Password),
    Result = TestUser:save(),
    {ok, [Result]}.

Example Authenticated Controller

In cases where we want an entire controller to require authentication, let's have ChicagoBoss make our lives a little bit easier:

%% file: src/controller/test_index_controller.erl
-module(test_index_controller, [Req]).
-compile(export_all).

%% Forces login if valid session is not present.
%% Called before all actions.
before_(_) ->
    user_lib:require_login(Req).

%%
%% Index
%%
%% requires TestUser
%%
%% GET index/index
%%
index('GET', [], TestUser) ->
    {ok, [{test_user, TestUser}]}.

before_/1 is doing most of the legwork here. It's called before any of our actions, calling user_lib:require_login in the process. Note, we can have before_ pass our TestUser to our actions by adding TestUser as a third parameter to our index function. This isn't necessary, but if you want to pass the model along to you views, this would be the place to do it.

Wrapping Up

Now you can start securing your ChicagoBoss applications and not have to use MD5 hashes (whoo!). I went through quite a few iterations in getting this to actually run without problems, more than likely due to my lack of experience with Erlang, so drop me a note if you run into any issues.

Shane Logsdon
Technical Product Leader

Technical product leader with 15+ years of experience in developer platforms and payment systems. Helping companies build scalable, secure, and customer-led financial solutions.