Storing Plug sessions with Mnesia
I am currently writing my second Phoenix application, Kakte. This is the first one with full-featured user management, so I came to ask myself how sessions are actually handled in the Elixir world. A session is a way to associate a state with an HTTP connection, so it is a pretty important feature which enables authentication and authorisation.
Before coming to Phoenix and starting to write some cool stuff, the only web technology I had played with was PHP. In its world, session information is stored server-side by default. A cookie containing a session ID is sent to the client, so that it can refer to the session on every request. So, what about Phoenix? I found something like this in my endpoint:
This means two things: the whole session information is stored in a cookie named _my_app_key, and it is signed so that the user cannot modify it.
There are two main advantages to store the session in a cookie:
- there is no need to handle session management on the server,
- in case of a distributed system, there is no need to synchronise sessions across servers as their content is transmitted in every request by the client.
However, this leads to security considerations. First, the user has access to the session content: this can reveal information you do not want to share. Yes, you can encrypt the cookie content — you should — but it still reveals the amount of information stored in the cookie. Second, you cannot remotly change the session without the user’s consent. Imagine that you store the user’s role in the session to avoid accessing the database on each request just to know if he can access a given page. Now, imagine you downgrade a user. If he wants to keep his old role, he just has to keep the old cookie. You could add an expiration date, but you cannot easily invalidate a session stored in a cookie; it would not take effect instantly.
So what if I want to store the session server-side in Elixir? Reading the Plug documentation about sessions, I found that you can change the store. In addition to the cookie store, there is an ETS store built-in. Yet, this store is not recommended: ETS does not share tables across nodes and data does not persist upon application reboot. Reading about this, I found that Redis seems to be the way to go when you want to store in-memory data for quick access, which can be shared between different servers and persist in case of server restart. So I found a Plug session store using Redis and opened the Redis documentation in a new tab to start learning on next morning. After the night, with a fresh mind and a bowl of Weetabix ready to be eaten, a question popped up: isn’t there a built-in database named Mnesia in OTP? Do I really need Redis? I made some research and read this article which basically says “maybe no”. Not knowing Redis already, I felt more comfortable with the idea using an application from OTP rather than a new dependency. Moreover, the latency would be better thanks to Mnesia sharing the same memory space as my Phoenix application. I looked for a Plug session store using Mnesia, but there was not. I decided it was time to write my first Hex package.
After learning Mnesia from the documentation, the first thing to do was to implement a new Plug.Session.Store. I took my inspiration in the ETS one, trading the deprecated Erlang timestamp struct with a good old Unix-styled integer timestamp. This is easier to work with when you want to do some date arithmetics. This session store is usable by itself:
Yet, having a Plug session store is not sufficient: you must create the Mnesia table and clean inactive sessions.
As I am lazy enough to automate things, I thought writing a few lines of code in every application using this store would be way too much. Having a simple Mix task to create the Mnesia table for me would be better. The workflow would be as follows:
- add :plug_session_mnesia to your depencencies,
- configure it,
- run a Mix task to create the table according to your configuration.
For this to work, it was necessary to state the table in the configuration. When stated there, it is no needed anymore to state it in the Plug.Session configuration:
# In the endpoint
# In config.exs
This way, the plug configuration becomes minimalist. To create the Mnesia table, you just have to run:
$ mix session.setup
This Mix tasks simply calls a helper function you can call yourself in your code. This can be useful when you deploy your release without Mix, for running it on the application startup. If the table already exists, it simply does nothing. Oh no, not exactly: this is only true if the table has the correct format. Else, you get an error stating this name is already used by a different table. Also useful to avoid runtime errors.
Using the store alone, sessions would accumulate forever. This is bad. After a certain time of inactivity, we want to delete a session. Having a timestamp which is updated on each access permits this. I have written a simple GenServer which regularly checks for inactive sessions and delete them. It is started automatically with the :plug_session_mnesia application, and must be configured:
max_age: 86400, # Delete sessions inactive for 1 day
cleaner_timeout: 60 # Check inactivity every minute. This setting
# is optional, 60 is the default value
I’ve had some fun in writing this little application, and I hope this will be useful for other people wanting to store the sessions server-side. Currently, it does not support table management for distributed systems out of the box, but I am sure it would be easy to add this. For those who want to use it in their applications, it is available on hex.pm and the source code is on GitHub. Let me know if you like it and if I can help you by adding something.
The next step for me will be to add persistent sessions support to my Phoenix application, with an ability to list them and remotely discard them. As I want to keep packages as atomic as possible, this would do a great second Hex package.