How do you invalidate/persist tokens with ash_authentication?

JWT tokens by itself are stateless, meaning that you can't invalidate them if you do not store them. Is there a way to do that with ash_authentication?
1 Reply
ZachDaniel
ZachDaniel•3y ago
Great question! There are options for this in the tokens DSL. THis is what it looks like in AshHq
tokens do
...
# These two options make it behave that way
store_all_tokens? true
require_token_presence_for_authentication? true
end
tokens do
...
# These two options make it behave that way
store_all_tokens? true
require_token_presence_for_authentication? true
end
There are other things you will likely want to do if you are doing this. These may be builtin at some point. In AshHq, we have a global change that we add to the resource that will remove all tokens when a user resets their password (thus logging out any other sessions).
# in your user resource
changes do
change AshHq.Accounts.User.Changes.RemoveAllTokens,
where: [action_is(:password_reset_with_password)]
end

# the change module
defmodule AshHq.Accounts.User.Changes.RemoveAllTokens do
@moduledoc """
Removes all tokens for a given user.

Since Ash does not yet support bulk actions, this goes straight to the data layer.
"""
use Ash.Resource.Change
require Ash.Query

def change(changeset, _opts, _context) do
Ash.Changeset.after_action(
changeset,
fn _changeset, user ->
# Ash doesn't support bulk updates yet, so
# we use this escape hatch to run an ecto query.
# if you're not using AshPostgres, you'll need to figure
# this part out on your own.
{:ok, query} =
AshHq.Accounts.UserToken
|> Ash.Query.filter(user_id == ^user.id)
|> Ash.Query.data_layer_query()

AshHq.Repo.delete_all(query)

{:ok, user}
end,
prepend?: true
)
end
end
# in your user resource
changes do
change AshHq.Accounts.User.Changes.RemoveAllTokens,
where: [action_is(:password_reset_with_password)]
end

# the change module
defmodule AshHq.Accounts.User.Changes.RemoveAllTokens do
@moduledoc """
Removes all tokens for a given user.

Since Ash does not yet support bulk actions, this goes straight to the data layer.
"""
use Ash.Resource.Change
require Ash.Query

def change(changeset, _opts, _context) do
Ash.Changeset.after_action(
changeset,
fn _changeset, user ->
# Ash doesn't support bulk updates yet, so
# we use this escape hatch to run an ecto query.
# if you're not using AshPostgres, you'll need to figure
# this part out on your own.
{:ok, query} =
AshHq.Accounts.UserToken
|> Ash.Query.filter(user_id == ^user.id)
|> Ash.Query.data_layer_query()

AshHq.Repo.delete_all(query)

{:ok, user}
end,
prepend?: true
)
end
end
Finally, you'll want to remove the token in your AuthController where the user signs out (or however you're signing out if you've done something custom). This is what it looks like in AshHq.
def sign_out(conn, _params) do
return_to = get_session(conn, :return_to) || "/"

token = Plug.Conn.get_session(conn, "user_token")

if token do
AshHq.Accounts.UserToken
|> AshAuthentication.TokenResource.Actions.get_token(%{"token" => token})
|> case do
{:ok, [token]} ->
AshHq.Accounts.UserToken.destroy!(token, authorize?: false)

_ ->
:ok
end
end

conn
|> clear_session()
|> redirect(to: return_to)
end
def sign_out(conn, _params) do
return_to = get_session(conn, :return_to) || "/"

token = Plug.Conn.get_session(conn, "user_token")

if token do
AshHq.Accounts.UserToken
|> AshAuthentication.TokenResource.Actions.get_token(%{"token" => token})
|> case do
{:ok, [token]} ->
AshHq.Accounts.UserToken.destroy!(token, authorize?: false)

_ ->
:ok
end
end

conn
|> clear_session()
|> redirect(to: return_to)
end
I'd like for this behavior to be built in as options eventually, but until then we have the tools to do what we want 😄

Did you find this page helpful?