As I’m mentioned before I have an ongoing project being developed using Elixir and Phoenix. I’m using two third party packages that I wanted to write about in this post. They are Guardian and Canary.
User Authentication and Authorization
In my app I’m using Guardian for for authentication and session management. Guardian has a lot of interesting functionality but at the moment I’m using it for simple password based authentication of users. And, for authorization I’m using the Canary library.
If you aren’t familiar with these two terms I’ll explain.
I use authentication to manage logins and sessions. The user authenicates him or herself by providing a user name and password. Once authenticated a session is created on the user’s behalf.
Once logged in the user has a certain set of privileges. For example, a user may be allowed to modify certain resources that he or she owns. But that same user may not be allowed to modify resources that belong to other users. Canary checks to see if a user is authorized to cary out the actions that he or she requests.
Guardian Setup
My Guardian configuration is pretty simple. I wrote up these pipelines for my router for Guardian:
pipeline :browser_session do
plug Guardian.Plug.VerifySession
plug Guardian.Plug.LoadResource
end
pipeline :require_login do
plug Guardian.Plug.EnsureAuthenticated, handler: MyApp.GuardianErrorHandler
end
Then I have created scopes that either support a logged in user or require a logged in user. For example:
scope "/", MyApp do
pipe_through [:browser, :browser_session]
get "/", PageController, :index
resources "/search", SearchController, only: [:index, :show]
end
This scope user the :browser_session
pipeine to load a session if one exists.
But it doesn’t require a session. This way both logged in users or guest users
can browse the home page and search.
But the resouces in this scope require a user to be logged in:
scope "/", MyApp do
pipe_through [:browser, :browser_session, :require_login]
resources "/rentals", RentalController do
resources "/images", ImageController, only: [:create]
end
end
Here, I add the :require_login
pipeline which uses Guardian’s EnsureAuthenticated
plug to require a session. Errors are passed off to my
MyApp.GuardianErrorHandler
module which is responsible for redirecting guest
users to /login
. This is a simple module that looks like this:
defmodule MyApp.GuardianErrorHandler do
import MyApp.Router.Helpers
def unauthenticated(conn, _params) do
conn
|> Phoenix.Controller.put_flash(:error,
"You must be logged in to access that page.")
|> Phoenix.Controller.redirect(to: login_path(conn, :new))
end
end
My Canary Setup
I use canary in my controllers to check if a user has authorization to view or modify a given resource.
For example, above we saw routes that edit rentals and create images for those rentals. And that these routes require a logged in user. Well, we need more than just a logged in user. The user needs to own those resources.
Here’s an example of how I use Canary to authorize for Images:
defmodule MyApp.RentalController do
use MyApp.Web, :controller
alias MyApp.Rental
plug :scrub_params, "rental" when action in [:create, :update]
plug :authorize_resource, model: Rental
# Actions follow ...
end
Canary’s :authorize_resource
plug checks if the current user is allowed to act on the a Rental
. This of course depends on a can?
function. Here’s part of the can?
function I’ve written:
def can?(user, action, Rental) when action in [:new, :create] do
user.role == "owner"
end
def can?(user, action, rental = %Rental{}) do
rental.owner_id == user.id
end
Here I decide if the user is authorized depending on the action.
Using Canary with Guardian
All of the above seems pretty great, but it doesn’t work!
One of the problems I had making Guardian and Canary play well together was that
Canary depends on being able to find the current user in the connection.
Guardian maintains the current user and it can be accessed by calling
Guardian.Plug.current_resource(conn)
. But, Canary expects the current user in
the :current_user
assign in the conn.
To make this work I wrote up another small plug:
defmodule MyApp.Plug.CurrentUser do
def init(opts), do: opts
def call(conn, _opts) do
current_user = Guardian.Plug.current_resource(conn)
Plug.Conn.assign(conn, :current_user, current_user)
end
end
This plug simply queries the current user from Guardian and put’s it into the :current_user
assign in the conn
. This copies the data from Guardian’s location to the location Canary expects.
I add this plug to my :require_login
pipeline
pipeline :require_login do
plug Guardian.Plug.EnsureAuthenticated, handler: MyApp.GuardianErrorHandler
plug MyApp.Plug.CurrentUser
end
With this plug Canary can access :current_user
as an assign and all is well. I
also use the :current_user
assign in my own code. My controllers don’t need to
know about Guardian.
Next Steps
This post focused on how I use Guardian and Canary together to authenticate and authorize in my app. I covered all of my Guardian usage and what I had to do in order to use the two libraries together. But, there’s a bit more to my Canary usage that I hope to write about next week.