dsincl12

dsincl12

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?

Showing Posts 1 to 10

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.

dsincl12

dsincl12 OP

My focus is primarily on specific use-cases like password resets, email updates, and account confirmations. I fully agree that this should not be used for session tokens.

Having a mostly empty sent_to column in the users_tokens table seems inefficient and somewhat inelegant to me.

Given these considerations, I find the stateless token approach to be a more efficient and cleaner solution for these particular scenarios.

sodapopcan

sodapopcan

I’m curious as to what you don’t like about the separate table solution as you didn’t really say. I like that it holds a record of a real-life event waiting to happen and once it happens, the row is deleted without a trace. It seems pretty unobtrusive. It also makes it trivial to build dashboards around these things.

dsincl12

dsincl12 OP

Poorly worded by me.

I don’t have an issue with the user_sessions table per se, but the sent_to column feels misplaced, especially when it remains empty for the majority of entries.

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.

sodapopcan

sodapopcan

Ha, I get it. It seems a bit more OCD—which I very, very much sympathize with—than an actual problem, though. It’s really nice being able to revoke tokens by simply deleting a db record!

dsincl12

dsincl12 OP

The email is in the payload so that wouldn’t work. And with inefficient I more meant in the overhead needed to manually deal with the short lived tokens (database cleanup etc) which would be handled automagically by the tokens themselves in my example.

Anyway I appreciate the feedback and somewhat agree that it could be confusing.

dsincl12

dsincl12 OP

Not really OCD, but more exploring, questioning and improving an existing solution. I’ve never had to delete a token for account confirmation or a password reset but maybe I’ve been lucky? :slight_smile:

sodapopcan

sodapopcan

Honestly, me neither, but in theory it’s good :joy: More so what José said about having a unified solution as it extends to the UI where the same code works for revoking any kind of token. In any event, I certainly didn’t mean to dismiss your work and I do appreciate your exploration and desire to improve! I was actually working on a UI for this stuff today which is why I felt the urge to comment.

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.

— All posts loaded —

Where Next? Top

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...
2977 94592 917
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
heathen
Quite interesting article Google brought me. Didn’t find any mentions about it here. What do you think in general? Would you use togethe...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
axelson
Hi there! :wave: @frigidcode and I (but mostly him) have been running an Elixir Book club, we’re almost done with Designing Elixir Syste...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
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
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
georgeguimaraes
Just published claude-code-elixir, a plugin marketplace for Claude Code with Elixir support. These are the plugins I’ve been using for my...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews