Is there any recommended way of doing API authentication?

I’m working on a toy project and I’d like to add user authentication to regulate the access to its Phoenix API.

As a starting point, I found this post and I like the approach it proposes.

I also found that Phoenix has the facility to generate tokens (Phoenix.Token). I see that the post I linked above uses SecureRandom to generate tokens. Is it recommended to use the output of Phoenix.Token.sign instead? Given that it allows to check whether the token is expired or not it seems to be strictly better than a random number. That would also save me from needing to keep track of the active sessions in the database.

That approach should cover pretty much all I need. Later on I might want to invalidate tokens if the user explicitly logs out, but I could easily do that with a blacklist.

So, everything is good but that I also found out that there are a several competing authentication libraries and I wanted to check if any of those is the primary choice of the community and it’s recommended over the approach I’m thinking of adopting.

1 Like

I like to use guardian for this task

You can find examples on how to implement it…

4 Likes

Guardian is fairly easy to set up and Elixir School has a fairly nice and short walk-through. The creator of Elixir School is an Ueberauth/Guardian maintainer…

https://elixirschool.com/lessons/libraries/guardian/

3 Likes

Thanks, I’ll give it a try. It seems to fit well with my needs

Do note that Guardian tokens are JWT, meaning they are large and heavy and overkill for many cases. If you can store the authorization needed into the JWT so you can prevent hitting the database in the API then Guardian is worth it, else use Phoenix Tokens and authorize via the database or cache or so.

9 Likes

Do you use a lib for this or do you always roll your own?

I just roll my own since it is only a half-dozen or so lines of code anyway. Like here is the meat of mine:

  def verify_login(%Plug.Conn{}=conn) do
    with\
      token = Plug.Conn.get_session(conn, "account"),
      {:ok, {account_id, account_session}} <- verify("account", token, max_age: token_max_age()),
      true <- confirm_account_id_session?(account_id, account_session) do
      Plug.Conn.assign(conn, :account_id, account_id)
    else
      err -> normalize(err)
    end
  end
  def verify_login(token) when is_binary(token), do: verify("account_id", token, max_age: token_max_age())
  def verity_login(token) do
    Logger.warn("Invalid token typed passed to verity_login of:  #{inspect token}")
    {:error, :invalid_token}
  end

I can pass either a Plug.Conn or a raw token string to this, it then calls verify:

  def verify(salt, token, opts \\ []), do: Phoenix.Token.verify(get_token_key(), salt, token, opts)

Which just returns the data of the token. There are a dozen or so other little helper functions too, like I don’t store any permissions or so data in the token (but you could to save a database hit) and instead I just store a ‘session’ token inside the phoenix token, which I look up via:

  def get_account_id_session(account_id, account_session) do
    query =
      from s in DB.Account.Session,
      where: is_nil(s.removed_at) and
        ((s.id == ^account_id and s.token == ^account_session) or (s.id == ^account_session and s.token == ^account_id))
    Repo.one(query)
  end

I just confirm the account id and session id both (which is stored in the phoenix token) exist in the database (the functions that call these confirm that the age of the token is valid too for the given user and so forth). It is pretty simple stuff and honestly I’ve over-engineered it, you could do it in like 3 lines of code. ^.^;

8 Likes

Thanks! I’ll give this a try on something I’m working on.

@OvermindDL1 I’m really curious how your normalize/1 works. I’d like to introduce something similar into my code.

1 Like

That is just normalize from the exceptional library.

1 Like

Do you have the possibility to log out / invalidate the token in your application?
I feel like a blacklist takes me back to some kind of storage and I lose my Phoenix.Token advantage.

Or am I overthinking this and logging out could be done in the client while the token theoretically stays active?

In my system a token is just an account_id + session_id combo. To invalidate it I just mark it as deleted from the database. The client will get informed of it at next request with it.

Oh, that is nice :slight_smile:.
But you have to check the database.

Still not sure whether I should start with coherence (specifically this comment) or just roll my own. I’ll definitely need a recovery function, which comes with coherence. Tending towards making my own because it’s much cleaner.

Yep. For API endpoints where I don’t want to check the DB at all then I put a VERY short lifespan on such a token so it is only valid for seconds or a minute top.

1 Like

Hi Overmind,

Thanks for sharing this code… quick question. Is there a reason you are storing the data in a Phoenix token in the session then unwrapping it using verify?

Since Plug sessions are signed and optionally encrypted then persisted in httpOnly cookies already isn’t it good enough to save the session_id, account_id, and expiration_datetime directly in the session? On every request you can verify the session is not expired then hit the DB to verify the session_id and account_id match and are not revoked before saving the account_id into the assigns.

Just want to make sure I’m not missing something big… Thanks!

Because it is also used by websockets and the cookie-less API paths, thus no session data.

1 Like