dsincl12

dsincl12

Rails 7.1 generate_token_for functionality for Phoenix

Hi everyone,

I always jump around between languages and frameworks to see what is new and to keep my general knowledge up to date if I need to use a different language or framework.

Lately I’ve been spending some time with Rails 7.1 and got a small crush on the authentication work they’ve done.

Since I have a couple of Phoenix side projects that I’ve built my own auth for, the empty columns for confirmation_token and reset_password_token has been a bit of a nuisance and I don’t like the phx.gen.auth setup with a separate table to handle tokens either.

Anyway to the actual point. In Rails they now have generate_token_for which avoids the entire need for additional columns just to handle password reset, updating email or confirming an account.

I’ve written a small PoC in Elixir that essentially solves the same issue and would like to hear some feedback, thoughts or suggestions on this.

First the TokenFor module:

defmodule MyApp.TokenFor do
  @moduledoc """
  Token management for various purposes.
  """

  alias Phoenix.Token
  alias MyAppWeb.Endpoint

  @type token_definition :: %{
          expires_in: integer(),
          payload_func: function()
        }

  @spec generate_token_for(Ecto.Schema.t(), token_definition) :: String.t()
  def generate_token_for(model, %{expires_in: expires_in, payload_func: payload_func}) do
    payload = payload_func.(model)

    Token.sign(Endpoint, Endpoint.config(:secret_key_base), {model.id, payload},
      max_age: expires_in
    )
  end

  @spec find_by_token_for(String.t(), token_definition, (integer() -> Ecto.Schema.t() | nil)) :: {:ok, Ecto.Schema.t()} | {:error, atom()}
  def find_by_token_for(token, %{payload_func: payload_func}, fetch_model_func) do
    case Token.verify(Endpoint, Endpoint.config(:secret_key_base), token) do
      {:ok, {id, payload}} ->
        model = fetch_model_func.(id)

        cond do
          model && payload == payload_func.(model) -> {:ok, model}
          model -> {:error, :invalid}
          true -> {:error, :not_found}
        end

      {:error, :expired} ->
        {:error, :not_found}

      {:error, _} ->
        {:error, :invalid}
    end
  end
end

This gives us everything we need to create and find users for tokens generated.

On the User model we just add a function for the definition we need, for example:

  def password_reset_definition do
    %{
      expires_in: 15 * 60, # 15 minutes in seconds
      payload_func: fn model -> model.email end
    }
  end

We can then generate a token with:

token = TokenFor.generate_token_for(user, User.password_reset_definition())

and retrieve the user with:

TokenFor.find_by_token_for(token, User.password_reset_definition(), fn id -> Repo.get(User, id) end)

What do you think?

Most Liked Switch mode

josevalim

josevalim

Creator of Elixir

Keep in mind there is a security drawback from this implementation. You no longer have fine-grained control to revoke tokens on the server. This is particularly important for sessions: you want to be able to revoke individual sessions and avoid session replay attacks. Given we need to implement DB tokens for sessions (it is pretty much considered a security best practice), then reusing the same structure for passwords and confirmations is a small step with similar benefits.

christhekeele

christhekeele

It’s one of those things where it doesn’t much matter, until it really does—people who target your platform for abuse rarely do so gently when they discover it’s exploitable.

I’ve worked for a few companies where this has gone south overnight. On one, we had to freeze new signups and re-rewite a bunch of code because the situation was so bad. On the other, we simply had to blank out a few critical columns in a few critical rows.

Malicious ex-employees with a valid client-side session token rewriting content to send death threats, people farming your mailer to drive successful emails to boost the ranking of a domain as a sender to thwart spam filters, your password reset flow being exploited to send erection pill spam—none of it happens, until it does en masse, and that’s when you start to thank the stars for secure defaults!

I’d say it’s an elegant way to correctly model the problem domain. If you’re worried about inefficiency, that’s mostly a matter of indexing for runtime performance—consider a covering index—and for storage inefficiency, I think we’d all take a few extra bytes per user as an ounce of prevention, in exchange for the pound of cure that comes if you have to freeze operations to deal with an abuse vector.

josevalim

josevalim

Creator of Elixir

I can understand inelegant but I doubt it being inneficient.

It also plays an important security feature. Otherwise someone can do this:

  1. Create an account for i_own@example.com
  2. Receive the confirmation token
  3. Swap my email to i_dont_own@example.com
  4. Now confirm as i_dont_own@example.com

So tokens must be tied to an email and having the column there is a helpful reminder. If you don’t do it on your Phoenix.Token approach, then it is vulnerable.

In any case, I would go with your approach if we didn’t have the table. Otherwise having both will be more confusing. At the same time, I worry that providing those facilities and telling people to roll their own auth features will be full of pitfalls like above.

Where Next?

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2976 91332 914
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
AstonJ
Just a general thread to post chat/news/info relating to AI/ML stuff that may be relevant for Nx now or in the future. Got anything to sh...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New
zachdaniel
Introducing AshStorage! Attachment and file management that slots directly into your resources :smiling_face_with_sunglasses: I had hope...
New

We're in Beta

About us Mission Statement