How to: Access tokens and flexible User Roles in Phoenix 1.3

In a project that I recently did for a potential client, I wanted to have a system that was accessible anywhere with an internet connection, and only users with an access key could register.

I also wanted to have different “User Types”, like: Admin, Project Manager, and Commenter.

  • Admins can do everything on the website
  • Project Managers can add new projects and edit fields
  • Commenters can only comment on existing projects.

Admins can also create new Access Tokens for a given User Type. The key can then be shared via email or message to a new user, and the new user uses that key when registering on the website. Finally, the key would be consumed (either deleted from the database or marked with an “is_used” flag).

One of the new features in Phoenix is Contexts. When using one of the built-in generators for bootstrapping a project, you need to think a little ahead about how your systems are organized. For me, it took a little bit of trial and error but eventually it clicked. The benefits to all of Contexts and thinking ahead is that it makes extending or adding new features to the codebase much easier. Also, the project feels more organized in my opinion.

To show how to make something like this in Phoenix, let’s start with a new project. Note that this tutorial assumes that PostgreSQL is already installed and running.

If you want to follow along or check the final result yourself, the source code can be found at: https://github.com/dstreet26/medium-phoenix-accesskeys

1. Create a new phoenix project and initialize database

mix phx.new accesskeys
cd accesskeys
mix ecto.create

2. Dependencies

We’ll need to encrypt user passwords. There are guides on how to do this. I found this post to be very useful.

We’ll also need to generate something unique for the key itself. I chose UUID’s but something else can be chosen as long as the string is unique.

I’ve also chosen to use pbkdf2 instead of something better like bcrypt or argon2. I chose pbkdf2 because this is a tutorial, and it is the only hashing library that doesn’t require a C compiler. (I want this tutorial to work with as many users as possible, and I don’t want to alienate Windows users who have to set up “nmake” and “vcvarsall.bat”).

mix.exs

+{:comeonin, “~> 4.0”},
+{:pbkdf2_elixir, “~> 0.12”},
+{:uuid, “~> 1.1”},

Install the new dependencies:

mix deps.get

3. All the generators.

The order is important, and so is adding the resources to the router after each generation — or else phoenix and ecto will complain.

First, we’ll generate UserTypes. They have a type name and a string array of actions.

mix phx.gen.html Accounts UserType user_types type:string:unique actions:array:string

Add to router.ex

resources “/user_types”, UserTypeController

Migrate

mix ecto.migrate

Second, we’ll generate AccessKeys. They have a string and are good for a single UserType

mix phx.gen.html Accounts AccessKey access_keys access_key:string user_type_id:references:user_types

Add to router.ex

resources “/access_keys”, AccessKeyController

Migrate

mix ecto.migrate

Third, we’ll generate Users. They have a name, email, encrypted_password, and a reference to a UserType

mix phx.gen.html Accounts User users name:string email:string:unique encrypted_password:string user_type_id:references:user_types

Add to router.ex

resources “/users”, UserController

Migrate

mix ecto.migrate

And that’s it for the generations! It’s a bit tedious but the result is a CRUD UI with a lot of boilerplate that is generated for us, saving lots of time!

4. Modifying the Ecto Schema

Before we go any further, lets add some associations and validations to our Ecto Schemas.

For the UserTypes, we want to add the has_many association to Users.

lib/accesskeys/accounts/user_type.ex

defmodule Accesskeys.Accounts.UserType do
use Ecto.Schema
import Ecto.Changeset
- alias Accesskeys.Accounts.UserType
+ alias Accesskeys.Accounts.{UserType,User}


schema "user_types" do
field :actions, {:array, :string}
field :type, :string
+ has_many :user, User

timestamps()
end

For the AccessKeys, we want to add the belongs_to association to the UserType

lib/accesskeys/accounts/access_key.ex

defmodule Accesskeys.Accounts.AccessKey do
use Ecto.Schema
import Ecto.Changeset
- alias Accesskeys.Accounts.AccessKey
+ alias Accesskeys.Accounts.{AccessKey,UserType}


schema "access_keys" do
field :access_key, :string
- field :user_type_id, :id
+ belongs_to :user_type, UserType

timestamps()
end

Finally, for Users, we want to add the belongs_to association to UserType. We also want to add a virtual field for passwords, add the password hashing routine to the changeset, and also add some simple validations — like making sure the email has an @ symbol and making sure that the password length is at least 5 characters.

lib/accesskeys/accounts/user.ex

defmodule Accesskeys.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
- alias Accesskeys.Accounts.User
+ alias Accesskeys.Accounts.{User,UserType}


schema "users" do
field :email, :string
field :encrypted_password, :string
field :name, :string
- field :user_type_id, :id
+ field :password, :string, virtual: true
+ field :access_key, :string, virtual: true
+ belongs_to :user_type, UserType

timestamps()
end
def changeset(%User{} = user, attrs) do
user
- |> cast(attrs, [:name, :email, :encrypted_password])
- |> validate_required([:name, :email, :encrypted_password])
+ |> cast(attrs, [:name, :email, :password])
+ |> validate_required([:name, :email, :password])
|> unique_constraint(:email)
+ |> validate_format(:email, ~r/@/)
+ |> validate_length(:password, min: 5)
+ |> put_change(:encrypted_password, hashed_password(attrs["password"]))
+ end
+
+ defp hashed_password(password) do
+ case password do
+ nil -> ""
+ _ -> Comeonin.Pbkdf2.hashpwsalt(password)
+ end
+ end

5. Set up UI for UserTypes with checkboxes for different actions

Here’s what the output will be when finished. The :index page shows a string array of the checked actions. The :new page shows a list of empty checkboxes. The :edit page shows the same thing with the checkboxes already checked.

Index page for User Types. Actions have to be modified to show the array properly.
Edit page for User Types. Checkboxes are pre-selected.

The database field for actions is an array of strings for the actions that that UserType can perform. However, for the HTML form, we need to have a checkbox list of all the available actions, and their selected state.

For example, these arrays are what gets stored in the database:

Project Manager: [“can_make_thumbnails”, “can_modify_fields”,”can_view_high_res_image”,”can_comment”]

And this is what we need to build for the edit page:

[{
action: "can_make_thumbnails",
isSelected: true
},
{
action: "can_add_users",
isSelected: false
},
{
action: "can_delete_users",
isSelected: false
},
{
action: "can_add_new_fields",
isSelected: false
},
{
action: "can_modify_fields",
isSelected: true
},
{
action: "can_view_high_res_image",
isSelected: true
},
{
action: "can_comment",
isSelected: true
}]

The actions themselves are simply hard-coded into the accounts module.

lib/accesskeys/accounts/accounts.ex

+  def valid_user_actions do
+ ["can_make_thumbnails",
+ "can_add_users",
+ "can_delete_users",
+ "can_add_new_fields",
+ "can_modify_fields",
+ "can_view_high_res_image",
+ "can_comment"
+ ]
+ end

The UserType form needs to show a checkbox list. With the checkboxes selected depending on the data.

lib/accesskeys_web/templates/user_type/form.html.eex

<div class="form-group">
<%= label f, :actions, class: "control-label" %>
<br>
<%= for valid_user_action <- @valid_user_actions do %>
<%= checkbox :checked_actions, "#{valid_user_action.action}", checked: valid_user_action.isChecked %> <%= "#{valid_user_action.action}" %> <br>
<%= end %>

<%= error_tag f, :actions %>
</div>

The controller will have some extra functions for creating an empty map for checkboxes,

lib/accesskeys_web/controllers/user_type_controller.ex

# Creates a default checkbox list from all the available actions
defp valid_user_actions_checkbox_list do
Accounts.valid_user_actions()
|> Enum.map(fn x -> %{action: x, isChecked: false} end)
end
# Creates a checkbox list from a string array with checkboxes set to true if they are in the array
defp get_selected_actions(current_actions) do
Accounts.valid_user_actions()
|> Enum.map(fn valid_action ->
%{
action: valid_action,
isChecked: Enum.any?(current_actions, fn(current_action) ->
current_action == valid_action
end
)}
end
)
end
# Creates a string array from a checkbox list with only the checked actions
defp filter_and_map_checked_actions(checked_actions) do
checked_actions
|> Enum.filter(fn {x,y} -> y == "true" end)
|> Enum.map(fn {x,y} -> x end)
end

new:

def new(conn, _params) do
# Get an Array of strings of the valid actions in the system
valid_user_actions = valid_user_actions_checkbox_list
  changeset = Accounts.change_user_type(%UserType{})
  render(conn, "new.html", changeset: changeset, valid_user_actions: valid_user_actions)
end

create:

def create(conn, %{"user_type" => user_type_params}) do
# Fallback if we have to go back to the :new page
valid_user_actions = valid_user_actions_checkbox_list
  # Turn the selected actions into just an array of strings
checked_actions = filter_and_map_checked_actions(conn.params["checked_actions"])
  # Add the filtered string array to the user_params
user_type_params = Map.put(user_type_params, "actions", checked_actions)
  case Accounts.create_user_type(user_type_params) do
{:ok, user_type} ->
conn
|> put_flash(:info, "User type created successfully.")
|> redirect(to: user_type_path(conn, :show, user_type))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset, valid_user_actions: valid_user_actions)
end
end

edit:

def edit(conn, %{"id" => id}) do
user_type = Accounts.get_user_type!(id)
#Create checkbox list with boxes pre-checked based on the current user_type
currently_selected_user_actions = get_selected_actions(user_type.actions)
changeset = Accounts.change_user_type(user_type)
render(conn, "edit.html", user_type: user_type, changeset: changeset, valid_user_actions: currently_selected_user_actions)
end

update:

def update(conn, %{"id" => id, "user_type" => user_type_params}) do
user_type = Accounts.get_user_type!(id)
# Fallback if we have to go back to the :edit page
currently_selected_user_actions = get_selected_actions(user_type.actions)
checked_actions = filter_and_map_checked_actions(conn.params["checked_actions"])
user_type_params = Map.put(user_type_params, "actions", checked_actions)
case Accounts.update_user_type(user_type, user_type_params) do
{:ok, user_type} ->
conn
|> put_flash(:info, "User type updated successfully.")
|> redirect(to: user_type_path(conn, :show, user_type))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html", user_type: user_type, changeset: changeset, valid_user_actions: currently_selected_user_actions)
end
end

Keep in mind that MOST of that code was already generated for us and the new code is passing arrays and structs around to where they need to go.

Finally, we should make the templates a little nicer for showing the array of checked actions:

lib/accesskeys_web/templates/user_type/index.html.eex

-      <td><%= user_type.actions %></td>
+ <td>[
+ <%= case user_type.actions do
+ nil -> ""
+ _ -> Enum.join(user_type.actions,", ")
+ end
+ %>
+ ]</td>

lib/accesskeys_web/templates/user_type/show.html.eex

-  <li>
- <strong>Actions:</strong>
- <%= @user_type.actions %>
+ <li><strong>Actions:</strong>
+ <ul>
+ <%= for action <- @user_type.actions do %>
+ <li><%= "#{action}" %></li>
+ <%= end %>
+ </ul>

6. Set up UI for Access Keys

Here’s what the Access Key interface will look like when we’re done. You can select a UserType from a dropdown, and an access key will be generated. This will be much less code than the previous section.

Generating a new Access key by using a dropdown for UserType.

For the Accounts module, we preload the user_types, we add a function to check if an access key exists, and modify our create_access_key to take a user_type_id.

lib/accesskeys/accounts/accounts.ex

def list_access_keys do
Repo.all(AccessKey)
+ |> Repo.preload(:user_type)
end
-  def get_access_key!(id), do: Repo.get!(AccessKey, id)
+ def get_access_key!(id), do: Repo.get!(AccessKey, id) |> Repo.preload(:user_type)
+
+ def check_access_key!(access_key) do
+ Repo.get_by(AccessKey, access_key: access_key)
+ end
-  def create_access_key(attrs \\ %{}) do
+ def create_access_key(attrs \\ %{}, user_type_id) do
%AccessKey{}
|> AccessKey.changeset(attrs)
+ |> Ecto.Changeset.put_change(:user_type_id, user_type_id)
|> Repo.insert()
end

For the AccessKey controller, we need to send the user_types to our “new” page. For the “create” page, we get the selected (dropdown) user_type_id, we generate a UUID, and we send that to the changeset.

lib/accesskeys_web/controllers/access_key_controller.ex

def new(conn, _params) do
+ user_types = Accounts.list_user_types()
changeset = Accounts.change_access_key(%AccessKey{})
- render(conn, "new.html", changeset: changeset)
+ render(conn, "new.html", changeset: changeset, user_types: user_types)
end
def create(conn, %{"access_key" => access_key_params}) do
- case Accounts.create_access_key(access_key_params) do
+ # Get the user_type_id from the selection box
+ {user_type_id, _ } = access_key_params["type"] |> Integer.parse
+
+ # Generate a unique string
+ key = UUID.uuid4()
+
+ # Add the string to the params
+ access_key_params = Map.put(access_key_params, "access_key", key)
+
+ case Accounts.create_access_key(access_key_params, user_type_id) do
{:ok, access_key} ->
conn
|> put_flash(:info, "Access key created successfully.")

For the AccessKey creation form, we need a selection dropdown

lib/accesskeys_web/templates/access_key/form.html.eex

<div class="form-group">
- <%= label f, :access_key, class: "control-label" %>
- <%= text_input f, :access_key, class: "form-control" %>
- <%= error_tag f, :access_key %>
- </div>
+ <%= inputs_for f, :user_type, fn utf -> %>
+ <%= label utf, :type, class: "control-label" %>
+ <%= select f, :type, @user_types|> Enum.map(&{&1.type, &1.id}) ,class: "form-control" %>
+ <%= error_tag utf, :type %>
+ <% end %>
+ </div>

For the AccessKey index page, we simply bring in the user_type because it’s already preloaded

lib/accesskeys_web/templates/access_key/index.html.eex

<tr>
<th>Access key</th>
+ <th>Good for</th>

<th></th>
</tr>
<tr>
<td><%= access_key.access_key %></td>
+ <td><%= access_key.user_type.type %></td>

<td class="text-right">

7. Set up UI for Users

We’ve come to the last step, the User registration page. Here’s what we’re going to achieve.

Creating a new User. Access key is required to Register.

As you can see, the UserType is automatically picked for us depending on the AccessKey that the user entered when creating their account. In this case, the token was good for a Project Manager.

In the Accounts module, we want to preload the UserType whenever we fetch users. We also want to modify the create_user to take a user_type_id

lib/accesskeys/accounts/accounts.ex

def list_users do
- Repo.all(User)
+ Repo.all(User) |> Repo.preload(:user_type)
end
-  def get_user!(id), do: Repo.get!(User, id)
+ def get_user!(id), do: Repo.get!(User, id) |> Repo.preload(:user_type)
-  def create_user(attrs \\ %{}) do
+ def create_user(attrs \\ %{}, user_type_id) do
%User{}
|> User.changeset(attrs)
+ |> Ecto.Changeset.put_change(:user_type_id, user_type_id)
|> Repo.insert()
end

In the controller, when creating a User, we first get the access key, check if it exists in the database, if it does, get the user_type_id from it and send it to the accounts module to create a new user.

lib/accesskeys_web/controllers/user_controller.ex

def new(conn, _params) do
+ user_types = Accounts.list_user_types()
changeset = Accounts.change_user(%User{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
- case Accounts.create_user(user_params) do
- {:ok, user} ->
+ # Get the access key from the params
+ access_key = user_params["access_key"]
+ # Check if it exists
+ database_key = Accounts.check_access_key!(access_key)
+
+ case database_key do
+ nil ->
conn
- |> put_flash(:info, "User created successfully.")
- |> redirect(to: user_path(conn, :show, user))
- {:error, %Ecto.Changeset{} = changeset} ->
- render(conn, "new.html", changeset: changeset)
+ |> put_flash(:error, "Access Key not found.")
+ |> redirect(to: user_path(conn, :index))
+ _ ->
+ user_type_id = database_key.user_type_id
+ case Accounts.create_user(user_params, user_type_id) do
+ {:ok, user} ->
+ Accounts.delete_access_key(database_key)
+ conn
+ |> put_flash(:info, "User created successfully.")
+ |> redirect(to: user_path(conn, :show, user))
+ {:error, %Ecto.Changeset{} = changeset} ->
+ render(conn, "new.html", changeset: changeset)
+ end
end
end

In the form, we add a new input for the AccessKey

lib/accesskeys_web/templates/user/form.html.eex

<div class="form-group">
- <%= label f, :encrypted_password, class: "control-label" %>
- <%= text_input f, :encrypted_password, class: "form-control" %>
- <%= error_tag f, :encrypted_password %>
+ <%= label f, :password, class: "control-label" %>
+ <%= password_input f, :password, class: "form-control" %>
+ <%= error_tag f, :password %>
+ </div>
+
+ <div class="form-group">
+ <%= label f, :access_key, class: "control-label" %>
+ <%= text_input f, :access_key, class: "form-control" %>
+ <%= error_tag f, :access_key %>
</div>

For the index and show pages, we don’t want to show the encrypted password. Instead, we want to show the UserType.

lib/accesskeys_web/templates/user/index.html.eex

<tr>
<th>Name</th>
<th>Email</th>
- <th>Encrypted password</th>
+ <th>Type</th>

<th></th>
</tr>
<td><%= user.email %></td>
- <td><%= user.encrypted_password %></td>
+ <td><%= user.user_type.type %></td>

<td class="text-right">

lib/accesskeys_web/templates/user/show.html.eex

<li>
- <strong>Encrypted password:</strong>
- <%= @user.encrypted_password %>
+ <strong>UserType:</strong>
+ <%= @user.user_type.type %>
</li>

8. Conclusion

So now we have a system where:

  1. Users can only register if they have an access key.
  2. The user’s UserType is set depending on which access key they were given.
  3. The UserType’s valid actions are flexible to Admins of the system.

Check out the repo for this project at: https://github.com/dstreet26/medium-phoenix-accesskeys

I am in the market for remote web development work. If you’re interested in hiring me, send me an email!

davidstreeterconsulting@gmail.com

https://davidstreeterconsulting.com