Phoenix basic auth exercise

I started learning Phoenix Framework recently. It’s simplicity is striking! I love it. I think it is a great framework to learn and to play with. Today I would like to show you how to implement basic authorization in Phoenix. It is not part of the Phoenix framework and this is one of the features you would like to use from time to time. It’s almost never the case that you want to keep basic auth for a longer period in production, but it’s definitely good to begin with.

It is quite easy to integrate Phoenix with libraries that already exist like basic_auth or simply search for what might hex.pm offer you. Still - I believe implementing basic auth on your own might be actually a good exercise and potentially you could end up with a bit more knowledge on how to build plugs.

Let’s start with building a dummy app to play with…

$ mix phoenix.new basic_auth_exercise

If you don’t have phoenix installed you might want to read the Phoenix installation article.

Specification

Take a look at Wikipedia and protocol section of Basic access authentication article. It expects us to do the following on the server side:

  1. throw HTTP 401 Unauthorized status when the request is unauthenticated
  2. send WWW-Authenticate: Basic realm="Thou Shalt not pass" header in the response

And from the client side there should be done only one thing:

  1. Set the Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l header in order to authenticate the user

The last part of the authorization header string is Base64 encoded username and password joined by the semicolon.

Implementation

Let’s open the test/controllers/page_controllers_test.exs - I think it’s perfect to put our tests there.

Nothing really interesting - we just have changed the description and change the expectations to match 401 unauthorized status we should get from the request. We also don’t expect 401 will be HTML response any more.

Now let’s fix the test. Before doing it let’s stop and think for a while how we would like to do - it. Maybe it would be better if we actually take a look at some article that says something about authorization. In there we see authorization is done as a function plug which is a part of the controller pipeline, but I think we want to make basic_auth a bit more generic and instead of controller it should go to the router level. Let’s put the following line to the :browser pipeline in web/router.ex file. Of course the tests would crash now as BasicAuth module is not even defined. You can see that we will implement a plug module. It will accept two params: :username and :password.

Let’s fix the mix test command by adding web/basic_auth.ex file (the tests would still be red thou). You can see that plug is a very simple module it only expects to define two functions: init/1 and call/2. The init/1 can be used for some heavy lifting jobs at the compile time while call/2 should be fast and simple as it is used in a runtime. We will use init/1 function to validate username and password existence. Authorization will be implemented in call/2.

We are ready to make the tests green again. Cool we used send_resp(conn, 401, "unauthorized") in order to set the 401 status and body to “unauthorized”. halt(conn) was used to set the halt connection flag to true (this “informs” other functions the connection has been stopped).

Let’s write another test. This one gives (RuntimeError) expected response with status 200, got: 401.

How do we know dXNlcjpzZWNyZXQ= is a valid authorization string? The answer is simple, let’s open the iex session and try the following:

iex(1)> Base.encode64("user:secret")
"dXNlcjpzZWNyZXQ="

This string needs to be valid!

It’s time to fix the tests again. Quite a few changes here:

  1. we pattern match on opts argument to get username and password quickly
  2. we are fetching the “authorization” header using get_req_header(conn, "authorization"). If the header is set and matches to ["Basic " <> auth] we let the connection through otherwise we send 401 status

Lets write another failing test scenario. For example we shouldn’t be authorized if authorization string is set to “Basic I like turtles”. This test fails unfortunately, but it’s not difficult to fix it. The tests are green again! The most important is the auth == encode(username, password) line where we are comparing the auth token with our secretly encoded string. Connection is being returned if there is a match, otherwise we return the unauthorized(conn) function which has been introduced to comply with DRY principle and we are still keeping pattern matching on “authorization” header without any change.

Lovely - we are now letting the authenticated users through and not authenticated users are getting 401 response status. We can test it by starting the server and navigating to http://localhost:4000/ in the web browser. Unfortunately even though we are getting the “unauthorized” message the browser doesn’t prompt with dialog to fill username and password.

There is one thing missing - a server response header WWW-Authenticate which should be sent when the user is not authenticated. Lets fix this by adding the relevant assertions to test cases.

The following implementation makes the tests green again The code speaks for itself here I think put_resp_header(conn, "www-authenticate", @realm) sets the correct response header.

Navigating to http://localhost:4000 after starting the server should result in following:

We are safe and secure

Testing with Elixir is fun

This step is completely optional. We would like to test and implement one more thing. The code should fail when we don’t pass :username or :password to our BasicAuth.init/1 function. Here is how the tests look like:

and the implementation is very simple

That’s it! Really the final BasicAuth module looks like that: And tests:

Conclusion

Just as rack in Rails the plug concept in Phoenix is simple but very powerful. Today we have learnt not only the basics of the plug module, but also how to implement quite useful basic authentication feature from scratch. I hope you enjoyed it.

The code could be found on github https://github.com/RadekMolenda/basic_auth_exercise

Happy Coding!