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 92995 915
New
caslu
I want to open this thread for you all to discuss and help those who really like Ash but are still hesitant to use it in a real project. ...
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
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
GES233
I’m posting this in response to Jose’s recent tweet (Cr. link) : People are sleeping on Elixir for a coding harness: Hot-code swappi...
New
_mfierro
Hello, I wrote Stop My Hand, a Scattergories-like web application using Phoenix/LiveView as my learning project for Elixir (after readin...
New
nseaSeb
AcmeScript — Writing JS hooks as if I were still using Elixir I’ve been having fun building a little something over the last few days: Ac...
New

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
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
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews