User Auth System with Persistent Sessions using Phoenix, Pow, and Mnesia

A Lib Abandoned

I have a web app built on Phoenix/Elixir that up until today used Coherence as its user authentication lib. Unfortunately the maintainer of this lib has straight ghosted, and so my options were fork Coherence and maintain it myself, build my own user auth system, or search out another lib. I did what I believed to be the smart and lazy choice and searched for something new. What I found was Pow.

New Lib on the Block

Pow is pretty dope. It has lots of great functionality out of the box, has controller callbacks with is a feature I felt Coherence was lacking, and is actively maintained. The documentation is lacking, but at the time of this writing, the project is very young, so I’m sure it will be added in due time.

One of Pow’s coolest upgrades over Coherence is that it has a built it module for persistent sessions that uses Mnesia. I wasn’t familiar with Mnesia before this, and from my cursory reading, it seems pretty much like SQLite but built into Erlang (and therefore Elixir) so that’s pretty cool. I was definitely excited to try implementing this. However, it was not without its stumbling blocks. Here’s how I got it done.

The First Hurdle

The Pow readme shows you what your Application module should look like if you want to use Mnesia across the board, but I only wanted to use it in production, so here’s how I accomplished that. Overall implementation is like a 6 of 10. Is it pretty? No. Does it work? Yes. Is it readable? Yes. So there you go. 6 of 10.

# lib/my_app/application.ex
defmodule MyApp.Application do
  use Application
  @pow_session_cache_worker Application.get_env(:my_app, :pow_session_cache_worker) || []

  def start(_type, _args) do
    import Supervisor.Spec

    pow_worker = @pow_session_cache_worker
    |> Enum.map(&worker(&1, [[nodes: [node()]]]))

    children = [
      supervisor(MyApp.Repo, []),
      supervisor(MyAppWeb.Endpoint, []),
    ] ++ pow_worker

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

  def config_change(changed, _new, removed) do
    MyAppWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end


# config/prod.exs
config :my_app, :pow_session_cache_worker, [Pow.Store.Backend.MnesiaCache]
config :my_app, :pow, cache_store_backend: Pow.Store.Backend.MnesiaCache

As you can see, I set the :pow_session_cache_worker config value just for prod, and every other environment will just be an empty list. I also added the :cache_store_backend config value as per the Pow docs.

I tried it out in dev first just to double check and everything was fine so time to deploy. I use Distillery and Edeliver, so I built a new release, deployed it to production, and restarted the production app, but there was a problem:

Application my_app exited: MyApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: Pow.Store.Backend.MnesiaCache 
** (EXIT) an exception was raised: 
    ** (UndefinedFunctionError) function :mnesia.create_schema/1 is undefined (module :mnesia is not available) 
        :mnesia.create_schema([:"my_app@127.0.0.1"]) 
        (pow) lib/pow/store/backend/mnesia_cache.ex:172: Pow.Store.Backend.MnesiaCache.table_init/1 
        (pow) lib/pow/store/backend/mnesia_cache.ex:66: Pow.Store.Backend.MnesiaCache.init/1 
        (stdlib) gen_server.erl:374: :gen_server.init_it/2 
        (stdlib) gen_server.erl:342: :gen_server.init_it/6 
        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

The Second Hurdle

A quick Google led me to this GitHub page that said “This is because distillery doesn’t export mnesia by default. You need to tell distillery to export :mnesia by adding it to the extra_applications option in your mix application.”

I tried that, and this is what I got:

Application my_app exited: MyApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: Pow.Store.Backend.MnesiaCache 
** (EXIT) an exception was raised: 
    ** (CaseClauseError) no case clause matching: {:aborted, {:bad_type, Pow.Store.Backend.MnesiaCache, :disc_copies, :"my_app@127.0.0.1"}} 
        (pow) lib/pow/store/backend/mnesia_cache.ex:179: Pow.Store.Backend.MnesiaCache.table_init/1 
        (pow) lib/pow/store/backend/mnesia_cache.ex:66: Pow.Store.Backend.MnesiaCache.init/1 
        (stdlib) gen_server.erl:374: :gen_server.init_it/2 
        (stdlib) gen_server.erl:342: :gen_server.init_it/6 
        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3 

Ok, this seems like… idk progress? The GitHub page was definitely right about Distillery not exposing :mnesia, but as we’ll see in a bit, their fix is not quite what I needed. Anyway, googling this new error message was not terribly productive.

A Hurdlish Stumble

One thing I did find is that remote consoling into my production app and running :mnesia.system_info() yielded some information – most interesting to me was that the mnesia directory was something like /home/my_app/app_releases/Mnesia.my_app@127.0.0.1. This is not really an intuitive place to keep my disk-written copy of the data, so another quick search revealed I should add this line to my config/prod.exs file:

config :mnesia, dir: '/home/my_app/app_storage/Mnesia'

I also had to mkdir the app_storage and Mnesia dirs on my production server. One really important thing to note is that YOU HAVE TO USE SINGLE QUOTES. Why does it want a charlist instead of a string? If there are any Erlangelicals (™ pending) out there, I’d love to know the answer. I’ll post the error I was getting when using a double-quoted directory string in config for any lost googlers out there:

Application my_app exited: MyApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: Pow.Store.Backend.MnesiaCache 
** (EXIT) an exception was raised: 
    ** (CaseClauseError) no case clause matching: {:error, {:EXIT, :function_clause}} 
        (pow) lib/pow/store/backend/mnesia_cache.ex:172: Pow.Store.Backend.MnesiaCache.table_init/1 
        (pow) lib/pow/store/backend/mnesia_cache.ex:66: Pow.Store.Backend.MnesiaCache.init/1 
        (stdlib) gen_server.erl:374: :gen_server.init_it/2 
        (stdlib) gen_server.erl:342: :gen_server.init_it/6 
        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

Still, this did not solve the 2nd error I got when I tried to start up the production app. Frustrated but unabated, I thought I’d try it out in dev again and see if I could piece together why one was working and the other was not.

The Finish Line

Lucky for me, I got the same error when I tried to start up the dev server. This meant it wasn’t some Distillery or production issue. After some experimentation, I discovered that having :mnesia in :extra_applications was the problem, because it was starting up Mnesia in memory, then when Pow tried to start it up with disk_copies enabled, it failed. But, I still needed Distillery to expose :mnesia to the rest of my app, so what to do?

The answer was with :included_applications. From the docs, “Any included application, defined in the :included_applications key of the .app file will also be loaded, but they won’t be started.” So I changed my application function in mix.exs to look like this:

  def application do
    [
      mod: {MyApp.Application, []},
      included_applications: [:mnesia]
    ]
  end

And boom! It worked like a difficult-to-debug charm.

Leave a Reply

Your email address will not be published. Required fields are marked *