Lessons on Building a Backend API in Phoenix — Plugs
By: Travis Petersen
Tags: Web Development, Elixir, Phoenix, Backend Development, Programming

…I said PLUGS

This post is a loose continuation of my previous post on implementing OAuth in a Phoenix backend API. Read it if you want to. I’m not your boss. Actually it’s more likely that you’re my boss, but I digress. Authentication is kind of pointless without leveraging session data to authorize users for given actions, so the logical next step upon implementing it is to create an authorization layer.

…but

There’s kind of an in-between step that I didn’t cover in the previous post — setting the current user in the session. This should happen in order to make data regarding the current session’s user available to controller actions that need it, and it’s worth going over. I’ll get to authorizations in the next post.

Let’s say we’re making a backend API for a basic blog, with a PostController that’s responsible for the CRUD actions for a Post resource. A Post belongs to a User , such that a signed-in user can create, edit, and delete their own posts. These actions are going to need information about the current session’s user in order to make the right associations and apply the right authorizations, so let’s add a test for PostController#create/2 to check that the current user was added to the session:

Normally, a controller’s tests probably wouldn’t be testing this functionality directly, but I think writing this test will help us better understand some of the concepts we’re covering. I’m using Plug.Test because I need it for init_test_session/2 , which I need in order to use functions like put_session/3 to manipulate the session for the test without getting errors barked at me about fetch_session/2 never having been called. Just try running this test without init_test_session/2 and you’ll see what I mean. Took me forever to figure out. Maybe it shouldn’t have, but it did. I create a User , initialize a session with an id token stub on line 19, and add their UUID to the session on line 20. Then I make a POST request with some dummy data to the route associated with PostController#create/2 , and assert on line 23 that the user stored in the session ( conn.assigns stores the session’s shared user data) matches the stubbed user.

Make sure to add use Plug.Test to the top of your test file and call init_test_session/2 if you want to stub out a session in your tests!

Currently, running this test will produce a failure something along the lines of

Assertion with == failed

code: assert conn.assigns()[:user] == user

left: nil

right: %BlogApi.Accounts.User{

__meta__: #Ecto.Schema.Metadata<:loaded, "users">,

...

because we’re not yet setting the user in the session. In order to get this test passing, we’re going to have to create a Plug.

Plugs and Pipelines

Phoenix Plugs can be used to operate on the conn struct that gets passed as the first argument to every controller action. Custom Plugs can be created to operate on conn before it is passed to a controller action. Phoenix automatically implements a number of Plugs for you when you create a new project. If you look at your router.ex file, you can see the Plugs that requests are being piped through. For instance, I have an :auth pipeline that implements a lot of the same plugs as the boilerplate :browser pipeline that gets generated when you create a new Phoenix project:

pipeline :auth do

plug :accepts, ["json"]

plug :fetch_session

plug :fetch_flash

plug :protect_from_forgery

plug :put_secure_browser_headers

end

This pipeline functions such that any scope that calls pipe_through :auth will pass conn through every one of these Plugs before it reaches the controller action. For instance :fetch_session will ensure that the current session data is available via conn to the controller actions that are associated with routes that pipe_through :auth :

scope "/auth", BlogApiWeb do

pipe_through :auth get "/signout", SessionController, :delete

get "/:provider", SessionController, :request

get "/:provider/callback", SessionController, :create

end

This way, my SessionController#create/2 action can utilize functions like put_session/3 without having to explicitly fetch_session in the controller. Which, quick aside, if you ever see the following error, it’s because you are trying to act on a session in a controller action that hasn’t been piped through a :fetch_session .

** (ArgumentError) session not fetched, call fetch_session/2

Pipelines are one way to implement Plugs, but this applies the plug to every single route in the scope , and I haven’t been able to find a way around that. I wanted to write custom Plugs that I could selectively apply to specific controller actions. I had a hard time figuring out how to do that even outside the pipeline, but eventually got it.

Function Plugs and Module Plugs

Plugs can be created as functions or as modules. Function Plugs are usually defined in a controller, and apply only to actions in that controller. These are functions that take conn and params as arguments and return conn after operating on it in some way. Defining Plugs in a module allows them to be utilized by multiple controllers, or implemented in a pipeline. These are modules that define two required functions, init/1 and call/2 . According to the docs,

init/1 …initializes any arguments or options to be passed to call/2 .

I haven’t implemented any custom functionality with it, so I cannot elaborate at this time. call/2 is the meat of the Plug. This is basically just a function plug — it takes conn and params as arguments and returns conn after operating on it in some way.

Function Plugs must be implemented within the controller in which they’re defined — and Module Plugs can be implemented in any controller — using the following syntax, given a Function Plug authorize_change/2 or a Module Plug AuthorizeChange :

# Function Plug:

plug :authorize_change when action in [:create, :update, :delete] # Module Plug:

plug BlogApi.Plugs.AuthorizeChange when action in [:create, :update, :delete]

The when action in condition is optional, but that is how you selectively apply a plug to a specific controller action or set thereof.

Setting the Current User

Now that we have an only moderately vague idea of what a Plug is and does and how to make one, let’s use one to add the current user to the session and get our test passing!

By convention, Plugs are stored in lib/<project_name>_web/plugs/ . We’ll create a file called set_user.ex and define a new module plug there:

This is fairly straightforward. If there’s already a user in the session, return conn (remember, Plugs always have to return conn ). Otherwise find the user based on the UUID that was stored in the session by SessionController#create/2 and store them in the session, or stick nil in there instead if no one’s logged in. Now all we have to do is add this Plug to our :api pipeline, and pipe our /api scope through it in router.ex :