A question that often comes up when I’m talking to Meteor developers about transitioning to Phoenix is how to handle authentication.
When transitioning, a developer with an existing application and data may want to integrate with Meteor’s existing authentication data in their Elixir/Phoenix application instead of jumping ship and switching to an entirely different authentication scheme.
Let’s dig into how Meteor’s password authentication works and how to use it within a Elixir/Phoenix application.
Setting Up Our Projects
To start, let’s assume that you have a Meteor application built with user accounts managed through the accounts-password
package.
For development purposes, let’s assume that your Meteor server is running locally on port 3000
, and your MongoDB database instance is running locally on port 3001
.
If you want to follow along, a quick way to set this up would be to clone the example Todos application and spin it up on your machine:
git clone https://github.com/meteor/todos
cd todos
meteor
Next, register a dummy user account (e.g., "user@example.com"
/"password"
) in your browser.
Now that Meteor has MongoDB running and populated with a Meteor-style user account, we’ll set up a new Phoenix project.
We’ll use Mix to create our application, and because we’re using MongoDB as our database, we’ll specify that we don’t want to use Ecto:
mix phoenix.new meteor_auth --no-ecto
Following the instructions in the mongodb
driver package, we’ll add dependencies on the mongodb
and poolboy
packages, and create a MongoPool
module.
Finally, we’ll add the MongoPool
to our list of supervised worker processes:
children = [
# Start the endpoint when the application starts
supervisor(MeteorAuth.Endpoint, []),
# Here you could define other workers and supervisors as children
worker(MongoPool, [[database: "meteor", port: 3001]])
]
After restarting our Phoenix server, our application should be wired up and communicating with our local MongoDB database.
Anatomy of Meteor Authentication
At first glance, Meteor’s password-based authentication system can be confusing.
However, once you untangle the mess of asynchronous, highly configurable and pluggable code, you’re left with a fairly straight-forward authentication process.
Authenticating an existing user usually begins with a call to the "login"
Meteor method. This method will call the login handler registered in the accounts-password
package, which simply does a password check. The result of the password check is passed into the _attemptLogin
function, which actually logs the user in if the password check was successful, or returns an error if the check was unsuccessful.
The results of a successful login are that the authenticated user will be associated with the current connection, and that the user’s _id
, resume token
, and a tokenExpires
timestamp will be returned to the client.
Building an Accounts Module
To support the ability to log into a Meteor application through Elixir, we’ll build a (hugely simplified) accounts module. The module will be responsible for transforming the email and password combination passed to the server into an authenticated user session.
Let’s start by defining the module and the module’s entry points:
defmodule MeteorAuth.Accounts do
def login(socket, %{
"user" => %{"email" => email},
"password" => password
}) when is_binary(email) and is_binary(password) do
socket
|> attempt_login(%{query: %{"emails.0.address": email}}, password)
end
end
The login
function in our MeteorAuth.Accounts
module will take in a Phoenix channel socket and a map that holds the user’s provided email address and password.
Notice that we're asserting that both email
and password
should be "binary" types? This helps prevent NoSQL injection vulnerabilities.
The login
function calls attempt_login
, which grabs the user from MongoDB based on the constructed query (get_user_from_query
), checks the user’s password (valid_credentials?
), and finally attempt to log the user in (log_in_user
):
defp attempt_login(socket, %{query: query}, password) do
user = get_user_from_query(query)
valid? = valid_credentials?(user, password)
log_in_user(valid?, socket, user)
end
To fetch the user document from MongoDB, we’re running a find
query against the "users"
collection, transforming the resulting database cursor into a list, and then returning the first element from that list:
defp get_user_from_query(query) do
MongoPool
|> Mongo.find("users", query)
|> Enum.to_list
|> List.first
end
To check the user’s password, we transform the user-provided password
string into a format that Meteor’s accounts package expects, and then we use the Comeonin package to securely compare the hashed version of the password string with the hashed password saved in the user’s document:
defp valid_credentials?(%{"services" => %{"password" => %{"bcrypt" => bcrypt}}},
password) do
password
|> get_password_string
|> Comeonin.Bcrypt.checkpw(bcrypt)
end
Notice how we’re using pattern matching to destructure a complex user document and grab only the fields we care about. Isn't Elixir awesome?
Before Bcrypt hashing a password string, Meteor expects it to be SHA256 hashed and converted into a lowercased base16 (hexadecimal) string. This is fairly painless thanks to Erlang’s :crypto
library:
defp get_password_string(password) do
:crypto.hash(:sha256, password)
|> Base.encode16
|> String.downcase
end
Our valid_credentials?
function will return either a true
or a false
if the user-provided credentials are correct or incorrect.
We can pattern match our log_in_user
function to do different things for valid and invalid credentials. If a user has provided a valid email address and password, we’ll log them in by assigning their user document to the current socket:
defp log_in_user(true, socket, user) do
auth_socket = Phoenix.Socket.assign(socket, :user, user)
{:ok, %{"id" => user["_id"]}, auth_socket}
end
For invalid credentials, we’ll simply return an error:
defp log_in_user(false, _socket, _user) do
{:error}
end
Logging in Through Channels
Now that our MeteorAuth.Accounts
module is finished up, we can wire it up to a Phoenix channel to test the end-to-end functionality.
We’ll start by creating a "ddp"
channel in our default UserSocket
module:
channel "ddp", MeteorAuth.DDPChannel
In our MeteorAuth.DDPChannel
module, we’ll create a "login"
event handler that calls our MeteorAuth.Accounts.login
function:
def handle_in("login", params, socket) do
case MeteorAuth.Accounts.login(socket, params) do
{:ok, res, auth_socket} ->
{:reply, {:ok, res}, auth_socket}
{:error} ->
{:reply, {:error}, socket}
end
end
If login
returns an :ok
atom, we’ll reply back with an :ok
status and the results of the login process (the user’s _id
).
If login
returns an :error
, we’ll reply back to the client with an error.
To make sure that everything’s working correctly, we can make another event handler for a "foo"
event. This event handler will simply inspect and return the currently assigned :user
on the socket:
def handle_in("foo", _, socket) do
user = socket.assigns[:user] |> IO.inspect
case user do
nil ->
{:reply, :ok, socket}
%{"_id" => id} ->
{:reply, {:ok, %{"id" => id}}, socket}
end
end
On the client, we can test to make sure that everything’s working as expected by running through a few different combinations of "foo"
and "login"
events:
let channel = socket.channel("ddp", {})
channel.join()
channel.push("foo")
.receive("ok", resp => { console.log("foo ok", resp) })
.receive("error", resp => { console.log("foo error", resp) })
...
channel.push("login", {user: {email: "user@example.com"}, password: "password"})
.receive("ok", resp => { console.log("login ok", resp) })
.receive("error", resp => { console.log("login error", resp) })
channel.push("foo")
.receive("ok", resp => { console.log("foo ok", resp) })
.receive("error", resp => { console.log("foo error", resp) })
And as expected, everything works!
We can now check if a user is currently authenticated on a socket by looking for the assigned :user
. If none exists, the current user is unauthenticated. If :user
exists, we know that the current user has been authenticated and is who they say they are.
Future Work
So far, we’ve only been able to log in with credentials set up through a Meteor application. We’re not creating or accepting resume tokens, and we’re missing lots of functionality related to signing up, logging out, resetting passwords, etc…
If your goal is to recreate the entirety of Meteor’s accounts package in Elixir/Phoenix, you have a long march ahead of you. The purpose of this article is to simply show that it’s possible and fairly painlessly to integrate these two stacks together.
It’s important to know that for green-field projects, or projects seriously planning on doing a full Elixir/Phoenix transition, there are better, more Phoenix-centric ways of approaching and handling user authentication and authorization.
That being said, if there’s any interest, I may do some future work related to resume tokens, signing up and out, and potentially turning this code into a more full-fledged Elixir package.
For now, feel free to check out the entire project on GitHub to get the full source. Let me know if there’s anything in particular you’d like to see come out of this!