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?
Trending in Discussions
Other Trending Topics
Latest Phoenix Threads
Chat & Discussions>Discussions
Latest on Elixir Forum
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #performance
- #security










Most Liked
josevalim
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
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
I can understand inelegant but I doubt it being inneficient.
It also plays an important security feature. Otherwise someone can do this:
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.