Patricio Cano

@patocano

AlloyCI Dev Diary — Part 4

User authentication with Guardian & Überauth

Photo by Matt Artz on Unsplash

This blog post is part of a series of posts detailing the development process of AlloyCI. The previous entries have been:

One of the first things you need for a non-trivial web application is user management, and authentication.

AlloyCI needs to connect to OAuth services, like GitHub, but also let users sign up with email and password. In the Rails ecosystem we have Devise. Devise takes care of the heavy lifting on user management, it encrypts passwords, makes it easy to recover passwords, you can validate user’s emails, before they can use their account, etc., and it also makes it super easy to integrate with OmniAuth, which allows you to let users sign up and login via services that implement OAuth.

My first instinct was to search for a similar library like Devise, but for the Phoenix/Elixir ecosystem. I was surprised when I couldn’t find anything as feature rich as Devise. There is a great discussion over at ElixirForum around what people use to authenticate users. This was my first source of information.

The first takeaway from that discussion is that there is a library very similar to Devise, and that is the Coherence Project by Stephen Pallen, but it’s still in early development, and has no support for OAuth. This was a blocker for me, as OAuth is needed for AlloyCI to communicate with GitHub properly.

The second takeaway was that there seems to be some consensus around what a lot of developers use for authentication, but there are not many resources for beginners to get started properly. The most used approach seems to be a mix between Guardian and Überauth. Both projects are maintained by the same group of people, so integrating them is not that complicated.

Guardian

Guardian uses JSON Web Tokens to authenticate users and authorize their actions. It provides the entire mechanism to create, encrypt, and validate JWTs. It gives you a way to keep a “session” active for a user, but it does not provide a method to actually manage them.

Überauth

Überauth allows you to create users from different authentication providers. It is compatible with a lot of OAuth services, and also provides a generic provider called “Identity” that allows your application to create users based on an email/password combination. It gives you the first entry point to authenticate a user, but it has no way of keeping a session active, or authorize actions.

Integrating Guardian and Überauth is key in order to get a proper authentication and session management system.

The good stuff

Now to the good stuff. How do you integrate Guardian & Überauth to an existing Phoenix application?

The code inside AlloyCI is based on the example application, but it was updated to be compatible with Phoenix 1.3.0 and Elixir 1.6.1.

1. Migrations

You should already have a user schema with the fields email and name . The password and other authorization related information will not be stored in the User schema, but under its own Authorization schema. Let’s create it:

This data structure will store information about the user’s authentication methods. (The example app calls them “authorizations”, because Guardian provides the option to encode permissions into the JWT it creates, so technically, you can also use it to authorize user’s actions, and not just authenticate them)

With the Authentication schema created, we need to modify the User schema to tell it it now has_many :authentications

Under normal circumstances, JWTs cannot be revoked, so if one of your tokens is compromised, you will need to switch your generation/validation function, thus invalidating all existing tokens. Guardian provides a workaround to this. By adding the GuardianDb dependency, Guardian will save your JWTs into the database, where you can easily revoke single tokens, should the need arise. A lot of people argue that this goes against the whole JWT schema, as it turns a session-less authentication schema, into a session-full one, but for the needs AlloyCI has, this approach works perfectly.

In order to use GuardianDb you need to create a new database table to hold the token information:

2. Controller

Now that we have the schemas ready, we can go ahead with the controllers. The example app already provides us with everything we need, so it’s just a matter of modifying it to fir our needs. This is what I did for AlloyCI initially. The first feature that I added was user login, since it’s the most necessary one for any online service.

Let’s start with the authentication controller, since this is the entry point for our authentication logic.

As you can see, this controller has actions to present a login page, handle the logout action, handle the callbacks from Überauth, and also generic callbacks for unauthenticated users, and unauthorized actions.

The Überauth callbacks handle the information that is sent from the OAuth provider. We use pattern matching in the function signatures to handle both successful and failed OAuth callbacks.

The way Überauth works is the following:

  • Registers a Plug that intercepts the raw HTTP request from the OAuth provider
  • Performs actions on the raw information and prepare special assigns for the request
  • Forwards the processed information to the controller

Depending on whether the OAuth callback succeeded or failed, the Überauth plug populates the corresponding assign. It is with this assign that we pattern match the request. If the callback failed, we redirect the user to the login screen and show an error message. If the action succeeded, we delegate to the Accounts module to do the actual heavy lifting.

This module takes care of reading the information sent by the OAuth provider and prepared by the Überauth Plug and returns a user object. This user could be a new user, or an existing user, and the job of determining which one is returned, falls on the Accounts module. This is handled by the get_or_create_user/2 function. You can have a look at the file, to see how everything ties together:

At this point, it is completely up to you how to handle what the application does with the information returned from the OAuth provider. In the case of GitHub, for AlloyCI, we create a new account if one doesn’t exist and populate it with the returned data, i.e. name, email, and username.

AlloyCI also allows users to be created via a regular account creation form, in this case, we ask the user for all this information, plus a password and password confirmation. For this to work with Überauth’s authentication procedure, it provides a generic Identity OAuth provider. This provider simulates a proper OAuth request, and sends all the information from the form. The password is then hashed and salted using Comeonin.Bcrypt hash algorithm.

If an account exists, then it is validated with the data already in the database. After the user is authenticated, the full application is available to them. The authentication is added to the session as a JWT Token with:

Guardian.Plug.sign_in(user, :access, perms: %{default:  Guardian.Permissions.max})

3. Routing

In order for the authentication filters to work, they need to be setup both on the controller and on the routes. In Rails, you would add a before_filter to your controller and call it a day, but in Phoenix there are two ways to do this, a routing pipeline, or as a controller specific plug.

Route Pipeline

A route pipeline allows you to send the request through a set of predefined plugs. The authentication pipeline for AlloyCI is defined in the routes.ex file like so:

As you can see from the comments, these plugs only check for the correct JWT, but if none is found, they don’t do anything else. This is on purpose, because we have resources that can be accessed without a token. For all other resources that need an authenticated user, an extra plug is needed. This is where the controller specific “filters” come in.

Controller Specific Plugs

You can define extra plugs on each controller to further filter the request. We use this on AlloyCI for controllers that need an authenticated user, and on the admin specific controllers. For the pipeline_controller.ex for example, we use one plug to make sure the user is logged in, and another to change the default layout, but only for the “show” action:

The public controller does not call any of the authentication plugs, since it does not need to have a user object, in order to show public information, like the landing page, or the registration page.

What is a Plug anyway?

A plug is a specification for composable modules between web applications. They allow you to share information between the modules, and manipulate it along the way. A Plug represents a connection with Plug.Conn and it is available throughout the request cycle. You can use a Plug to manipulate the request, extract information from it, add information to it, etc.

In the case the authentication Plugs, we get information from the connection session, validate it, and then add private claims to it. We then use this private claims in another Plug to load the current_user resource (this is all handled by the Guardian library). This is actually what the :browser_auth pipeline we showed you above does.

A more basic use of a Plug can be found here, where we get information from the connection header, and save it to the assigns inside the connection, in order to be able to pattern match against it:

This plug is declared inside a pipeline for the GitHub API handler, and is used in the github_event_controller.ex like this, where we pattern match against a “push” event where a reference was deleted:

4. Views & Templates

5. Tests

How you test your application is totally up to you, but if you want to test controllers that are behind the authentication wall, then you will need some helper methods to prepare the connection and populate it with the structs it needs for the tests:

These methods should be added to the conn_case.ex file and used within your controller tests, e.g pipeline_controller_test.exs:

This covers the basics of how Guardian and Überauth are integrated with AlloyCI, and should give you a starting point, and some insight, into how to integrate them with your project.

More by Patricio Cano

Topics of interest

More Related Stories