VYPR
Moderate severityNVD Advisory· Published Feb 11, 2025· Updated Feb 12, 2025

Ash Authentication has flawed token revocation checking logic in actions generated by `mix ash_authentication.install`

CVE-2025-25202

Description

Revoked tokens in Ash Authentication for Elixir are allowed to verify as valid when using installer-generated actions, enabling reuse of magic link, password reset, and confirmation tokens until their expiration.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Revoked tokens in Ash Authentication for Elixir are allowed to verify as valid when using installer-generated actions, enabling reuse of magic link, password reset, and confirmation tokens until their expiration.

CVE-2025-25202 affects Ash Authentication, an authentication framework for Elixir applications. The vulnerability arises from a flaw in the token revocation checking logic in actions generated by the mix ash_authentication.install igniter installer present since AshAuthentication v4.1.0 [1][2][4]. When a custom :revoked? generic action is generated by the installer, it fails to properly enforce revocation, allowing revoked tokens to be accepted as valid [2][3][4]. The internal :revoked? action within Ash Authentication itself is not affected and continues to work correctly [2].

Exploitation requires that an application was bootstrapped using the new igniter installer and uses the magic link strategy, password resets, confirmation add-on, or manually revokes tokens [2][4]. An attacker who obtains a valid token (for example, a magic link sent to a user) can reuse that token multiple times until it expires, because revocation is not enforced [4]. Magic link tokens have a default lifetime of 10 minutes, while password reset and confirmation tokens default to 3 days [4]. No authentication is needed beyond possession of the token itself.

The impact is that tokens intended for single-use (like magic links) can be reused, potentially allowing an attacker to authenticate multiple times or complete actions such as password resets with a previously used token [2][4]. The surface area is considered low for magic links due to the short expiration, but password reset and confirmation tokens have longer lifetimes [4]. The flaw is patched in version 4.4.9, which includes an upgrader and compile-time warnings with remediation instructions [2][3][4]. As a workaround, administrators can delete the generated :revoked? generic action in the token resource, reverting to the correct internal implementation [2].

Mitigation

Upgrade to Ash Authentication version 4.4.9 or later [2][4]. The patch includes an automatic upgrader via mix igniter.upgrade ash_authentication, or the upgrade can be run manually as described in error messages [2][3]. The fix ensures that the token revocation logic correctly validates the revoked status before allowing token use.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
ash_authenticationHex
>= 4.1.0, < 4.4.94.4.9

Affected products

2

Patches

1
2dee55252df2

fix: Ensure that installer generated token revocation checking action is correct. (#905)

9 files changed · +276 33
  • lib/ash_authentication/add_ons/confirmation/transformer.ex+1 1 modified
    @@ -22,7 +22,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do
         with :ok <-
                validate_token_generation_enabled(
                  dsl_state,
    -             "Token generation must be enabled for password resets to work."
    +             "Token generation must be enabled for confirmation to work."
                ),
              :ok <- validate_monitor_fields(dsl_state, strategy),
              strategy <- maybe_set_confirm_action_name(strategy),
    
  • lib/ash_authentication/token_resource.ex+2 1 modified
    @@ -145,7 +145,8 @@ defmodule AshAuthentication.TokenResource do
     
       use Spark.Dsl.Extension,
         sections: @dsl,
    -    transformers: [TokenResource.Transformer, TokenResource.Verifier]
    +    transformers: [TokenResource.Transformer],
    +    verifiers: [TokenResource.Verifier]
     
       @doc """
       Has the token been revoked?
    
  • lib/ash_authentication/token_resource/is_revoked.ex+25 4 modified
    @@ -1,13 +1,34 @@
     defmodule AshAuthentication.TokenResource.IsRevoked do
       @moduledoc """
    -  Checks for the existence of a revocation token for the provided tokenrevocation token for the provided token.
    +  Checks for the existence of a revocation token for the provided token revocation token for the provided token.
       """
       use Ash.Resource.Actions.Implementation
    +  alias Ash.Error.Action.InvalidArgument
    +  alias AshAuthentication.{Errors.InvalidToken, Jwt}
     
       @impl true
    -  def run(input, _, _) do
    -    input.resource
    -    |> Ash.Query.do_filter(purpose: "revocation", jti: input.arguments.jti)
    +  def run(%{resource: resource, arguments: %{jti: jti}}, _, _) when is_binary(jti) do
    +    resource
    +    |> Ash.Query.do_filter(purpose: "revocation", jti: jti)
    +    |> Ash.Query.set_context(%{
    +      private: %{ash_authentication?: true}
    +    })
         |> Ash.exists()
       end
    +
    +  def run(%{arguments: %{token: token}} = input, opts, context) when is_binary(token) do
    +    case Jwt.peek(token) do
    +      {:ok, %{"jti" => jti}} -> run(%{input | arguments: %{jti: jti}}, opts, context)
    +      {:ok, _} -> {:error, InvalidToken.exception(type: :revocation)}
    +      {:error, reason} -> {:error, reason}
    +    end
    +  end
    +
    +  def run(_input, _, _) do
    +    {:error,
    +     InvalidArgument.exception(
    +       field: :jti,
    +       message: "At least one of `jti` or `token` arguments must be present"
    +     )}
    +  end
     end
    
  • lib/ash_authentication/token_resource/verifier.ex+59 20 modified
    @@ -3,40 +3,79 @@ defmodule AshAuthentication.TokenResource.Verifier do
       The token resource verifier.
       """
     
    -  use Spark.Dsl.Transformer
    +  use Spark.Dsl.Verifier
       require Ash.Expr
    -  alias Spark.{Dsl.Transformer, Error.DslError}
    +  require Logger
    +  alias Spark.{Dsl.Verifier, Error.DslError}
       import AshAuthentication.Utils
    +  import AshAuthentication.Validations.Action
     
       @doc false
       @impl true
    -  @spec after?(any) :: boolean()
    -  def after?(_), do: true
    +  @spec verify(map) :: :ok | {:error, term}
    +  def verify(dsl_state) do
    +    with :ok <- validate_domain_presence(dsl_state) do
    +      maybe_validate_is_revoked_action_arguments(dsl_state)
    +    end
    +  end
     
    -  @doc false
    -  @impl true
    -  @spec before?(any) :: boolean
    -  def before?(_), do: false
    +  defp maybe_validate_is_revoked_action_arguments(dsl_state) do
    +    case Verifier.get_option(dsl_state, [:token, :revocation], :is_revoked_action_name, :revoked?) do
    +      nil ->
    +        :ok
     
    -  @doc false
    -  @impl true
    -  @spec after_compile? :: boolean
    -  def after_compile?, do: true
    +      action_name ->
    +        case validate_action_exists(dsl_state, action_name) do
    +          {:ok, action} -> validate_is_revoked_action(dsl_state, action)
    +          {:error, _} -> :ok
    +        end
    +    end
    +  end
     
    -  @doc false
    -  @impl true
    -  @spec transform(map) ::
    -          :ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
    -  def transform(dsl_state) do
    -    validate_domain_presence(dsl_state)
    +  defp validate_is_revoked_action(dsl_state, action) do
    +    with :ok <- validate_action_argument_option(action, :jti, :allow_nil?, [true]),
    +         :ok <- validate_action_argument_option(action, :token, :allow_nil?, [true]),
    +         :ok <- validate_action_option(action, :returns, [:boolean, Ash.Type.Boolean]) do
    +      :ok
    +    else
    +      {:error, _} ->
    +        Logger.warning("""
    +        Warning while compiling #{inspect(Verifier.get_persisted(dsl_state, :module))}:
    +
    +        The `:jti` and `:token` options to the `#{inspect(action.name)}` action must allow nil values and it must return a `:boolean`.
    +
    +        This was an error in our igniter installer previous to version 4.4.9, which allowed revoked tokens to be reused.
    +
    +        To fix this, run the following command in your shell:
    +
    +            mix ash_authentication.upgrade 4.4.8 4.4.9
    +
    +        Or:
    +
    +          - remove `allow_nil?: false` from these action arguments, and
    +          - ensure that the action returns `:boolean`.
    +
    +          like so:
    +
    +            action :revoked?, :boolean do
    +              description "Returns true if a revocation token is found for the provided token"
    +              argument :token, :string, sensitive?: true
    +              argument :jti, :string, sensitive?: true
    +
    +              run AshAuthentication.TokenResource.IsRevoked
    +            end
    +        """)
    +
    +        :ok
    +    end
       end
     
       defp validate_domain_presence(dsl_state) do
    -    with domain when not is_nil(domain) <- Transformer.get_option(dsl_state, [:token], :domain),
    +    with domain when not is_nil(domain) <- Verifier.get_option(dsl_state, [:token], :domain),
              :ok <- assert_is_module(domain),
              true <- function_exported?(domain, :spark_is, 0),
              Ash.Domain <- domain.spark_is() do
    -      {:ok, domain}
    +      :ok
         else
           nil ->
             {:error,
    
  • lib/mix/tasks/ash_authentication.install.ex+3 3 modified
    @@ -342,10 +342,10 @@ if Code.ensure_loaded?(Igniter) do
               end
               """)
               |> Ash.Resource.Igniter.add_action(token_resource, """
    -          action :revoked? do
    +          action :revoked?, :boolean do
                 description "Returns true if a revocation token is found for the provided token"
    -            argument :token, :string, sensitive?: true, allow_nil?: false
    -            argument :jti, :string, sensitive?: true, allow_nil?: false
    +            argument :token, :string, sensitive?: true
    +            argument :jti, :string, sensitive?: true
     
                 run AshAuthentication.TokenResource.IsRevoked
               end
    
  • lib/mix/tasks/ash_authentication.upgrade.ex+172 0 added
    @@ -0,0 +1,172 @@
    +# credo:disable-for-this-file Credo.Check.Design.AliasUsage
    +if Code.ensure_loaded?(Igniter) do
    +  defmodule Mix.Tasks.AshAuthentication.Upgrade do
    +    @moduledoc false
    +
    +    use Igniter.Mix.Task
    +
    +    @impl Igniter.Mix.Task
    +    def info(_argv, _composing_task) do
    +      %Igniter.Mix.Task.Info{
    +        # Groups allow for overlapping arguments for tasks by the same author
    +        # See the generators guide for more.
    +        group: :ash_authentication,
    +        # *other* dependencies to add
    +        # i.e `{:foo, "~> 2.0"}`
    +        adds_deps: [],
    +        # *other* dependencies to add and call their associated installers, if they exist
    +        # i.e `{:foo, "~> 2.0"}`
    +        installs: [],
    +        # An example invocation
    +        # example: __MODULE__.Docs.example(),
    +        example: "example",
    +        # a list of positional arguments, i.e `[:file]`
    +        positional: [:from, :to],
    +        # Other tasks your task composes using `Igniter.compose_task`, passing in the CLI argv
    +        # This ensures your option schema includes options from nested tasks
    +        composes: [],
    +        # `OptionParser` schema
    +        schema: [],
    +        # Default values for the options in the `schema`
    +        defaults: [],
    +        # CLI aliases
    +        aliases: [],
    +        # A list of options in the schema that are required
    +        required: []
    +      }
    +    end
    +
    +    @impl Igniter.Mix.Task
    +    def igniter(igniter) do
    +      positional = igniter.args.positional
    +      options = igniter.args.options
    +
    +      upgrades =
    +        %{
    +          "4.4.9" => [&fix_token_is_revoked_action/2]
    +        }
    +
    +      # For each version that requires a change, add it to this map
    +      # Each key is a version that points at a list of functions that take an
    +      # igniter and options (i.e. flags or other custom options).
    +      # See the upgrades guide for more.
    +      Igniter.Upgrades.run(igniter, positional.from, positional.to, upgrades,
    +        custom_opts: options
    +      )
    +    end
    +
    +    def fix_token_is_revoked_action(igniter, _opts) do
    +      case find_all_token_resources(igniter) do
    +        {igniter, []} ->
    +          igniter
    +
    +        {igniter, resources} ->
    +          Enum.reduce(resources, igniter, fn resource, igniter ->
    +            maybe_fix_is_revoked_action(igniter, resource)
    +          end)
    +      end
    +    end
    +
    +    defp find_all_token_resources(igniter) do
    +      Igniter.Project.Module.find_all_matching_modules(igniter, fn _module, zipper ->
    +        with {:ok, zipper} <- Igniter.Code.Module.move_to_use(zipper, Ash.Resource),
    +             {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
    +             {:ok, zipper} <- Igniter.Code.Keyword.get_key(zipper, :extensions) do
    +          if Igniter.Code.List.list?(zipper) do
    +            match?(
    +              {:ok, _},
    +              Igniter.Code.List.move_to_list_item(
    +                zipper,
    +                &Igniter.Code.Common.nodes_equal?(&1, AshAuthentication.TokenResource)
    +              )
    +            )
    +          else
    +            Igniter.Code.Common.nodes_equal?(zipper, AshAuthentication.TokenResource)
    +          end
    +        end
    +      end)
    +    end
    +
    +    defp maybe_fix_is_revoked_action(igniter, resource) do
    +      Igniter.Project.Module.find_and_update_module!(igniter, resource, fn zipper ->
    +        with {:ok, action_zipper} <- move_to_action(zipper, :action, :revoked?),
    +             {:ok, zipper} <- Igniter.Code.Common.move_to_do_block(action_zipper),
    +             {:ok, zipper} <- remove_argument_option(zipper, :token, :allow_nil?),
    +             {:ok, zipper} <- remove_argument_option(zipper, :jti, :allow_nil?) do
    +          add_action_return_type(zipper, :boolean)
    +        else
    +          :error -> {:ok, zipper}
    +        end
    +      end)
    +    end
    +
    +    defp move_to_action(zipper, type, name) do
    +      Igniter.Code.Function.move_to_function_call(
    +        zipper,
    +        type,
    +        2,
    +        &Igniter.Code.Function.argument_equals?(&1, 0, name)
    +      )
    +    end
    +
    +    defp add_action_return_type(zipper, type) do
    +      zipper = Sourceror.Zipper.top(zipper)
    +
    +      with {:ok, zipper} <- move_to_action(zipper, :action, :revoked?),
    +           {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1) do
    +        {:ok,
    +         Sourceror.Zipper.insert_left(
    +           zipper,
    +           quote do
    +             unquote(type)
    +           end
    +         )}
    +      end
    +    end
    +
    +    defp remove_argument_option(zipper, argument_name, option) do
    +      with {:ok, zipper} <-
    +             Igniter.Code.Function.move_to_function_call(
    +               zipper,
    +               :argument,
    +               3,
    +               &Igniter.Code.Function.argument_equals?(&1, 0, argument_name)
    +             ),
    +           {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 2) do
    +        if Igniter.Code.List.find_list_item_index(
    +             zipper,
    +             &Igniter.Code.Tuple.elem_equals?(&1, 0, :do)
    +           ) do
    +          Igniter.Code.Common.within(zipper, fn zipper ->
    +            {:ok,
    +             Igniter.Code.Common.remove(
    +               zipper,
    +               &Igniter.Code.Function.function_call?(&1, option, 1)
    +             )}
    +          end)
    +        else
    +          Igniter.Code.List.remove_from_list(
    +            zipper,
    +            &Igniter.Code.Tuple.elem_equals?(&1, 0, option)
    +          )
    +        end
    +      end
    +    end
    +  end
    +else
    +  defmodule Mix.Tasks.AshAuthentication.Upgrade do
    +    @moduledoc false
    +
    +    use Mix.Task
    +
    +    def run(_argv) do
    +      Mix.shell().error("""
    +      The task 'ash_authentication.upgrade' requires igniter. Please install igniter and try again.
    +
    +      For more information, see: https://hexdocs.pm/igniter/readme.html#installation
    +      """)
    +
    +      exit({:shutdown, 1})
    +    end
    +  end
    +end
    
  • test/mix/tasks/ash_authentication.install_test.exs+3 3 modified
    @@ -189,10 +189,10 @@ defmodule Mix.Tasks.AshAuthentication.InstallTest do
               prepare(AshAuthentication.TokenResource.GetTokenPreparation)
             end
     
    -        action :revoked? do
    +        action :revoked?, :boolean do
               description("Returns true if a revocation token is found for the provided token")
    -          argument(:token, :string, sensitive?: true, allow_nil?: false)
    -          argument(:jti, :string, sensitive?: true, allow_nil?: false)
    +          argument(:token, :string, sensitive?: true)
    +          argument(:jti, :string, sensitive?: true)
     
               run(AshAuthentication.TokenResource.IsRevoked)
             end
    
  • test/support/example/token.ex+9 0 modified
    @@ -12,5 +12,14 @@ defmodule Example.Token do
     
       actions do
         defaults [:read, :destroy]
    +
    +    action :revoked? do
    +      description "Returns true if a revocation token is found for the provided token"
    +      argument :token, :string, sensitive?: true
    +      argument :jti, :string, sensitive?: true
    +
    +      run AshAuthentication.TokenResource.IsRevoked
    +      returns :boolean
    +    end
       end
     end
    
  • .vscode/settings.json+2 1 modified
    @@ -7,6 +7,7 @@
         "Marties",
         "moduledocs",
         "oidc",
    -    "unguessable"
    +    "unguessable",
    +    "zipper"
       ]
     }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.