RicoTrevisan

RicoTrevisan

Trying to grok ash-authentication -- I keep getting forbidden

I can’t quite get my head around what I need to do to make this test pass. Although I took the Ash training last year, I never had the chance to put it to practice so all that learning melted away – either due to time passing or all those beers @zachdaniel treated us at Elixir Conf EU.

Here’s the test

test "can save cookie" do
  user = generate(confirmed_user())

  assert {:ok, updated_user} =
           user
           |> Ash.Changeset.for_update(:add_cookie, %{cookie: "some_cookie"}, actor: user)
           |> Ash.update()

  assert updated_user.cookie == "some_cookie"
end

I get the error:

...
bread_crumbs: ["Error returned from: Bubblelog.Accounts.User.add_cookie"],
class: :forbidden"
...
Full error
%Ash.Error.Forbidden{
  bread_crumbs: ["Error returned from: Bubblelog.Accounts.User.add_cookie"],
  changeset: "#Changeset<>",
  errors: [
    %Ash.Error.Forbidden.Policy{
      scenarios: [],
      facts: %{
        false => false,
        true => true,
        {Ash.Policy.Check.Action, [action: [:add_cookie], access_type: :filter]} => true,
        {Ash.Policy.Check.ActorPresent, [access_type: :filter]} => true,
        {AshAuthentication.Checks.AshAuthenticationInteraction,
         [access_type: :filter]} => false
      },
      filter: nil,
      actor: %Bubblelog.Accounts.User{
        id: "617873b3-5013-4a49-8789-b801d747d34f",
        email: #Ash.CiString<"simple0@example.com">,
        confirmed_at: ~U[2025-06-18 07:25:37.147928Z],
        __meta__: #Ecto.Schema.Metadata<:loaded, "users">
      },
      policy_breakdown?: false,
      must_pass_strict_check?: false,
      for_fields: nil,
      subject: #Ash.Changeset<
        domain: Bubblelog.Accounts,
        action_type: :update,
        action: :add_cookie,
        attributes: %{cookie: "some_cookie"},
        relationships: %{},
        errors: [],
        data: %Bubblelog.Accounts.User{
          id: "617873b3-5013-4a49-8789-b801d747d34f",
          email: #Ash.CiString<"simple0@example.com">,
          confirmed_at: ~U[2025-06-18 07:25:37.147928Z],
          __meta__: #Ecto.Schema.Metadata<:loaded, "users">
        },
        valid?: true
      >,
      context_description: nil,
      policies: [
        %Ash.Policy.Policy{
          condition: [
            {AshAuthentication.Checks.AshAuthenticationInteraction,
             [access_type: :filter]}
          ],
          policies: [
            %Ash.Policy.Check{
              check: {Ash.Policy.Check.Static, [result: true]},
              check_module: Ash.Policy.Check.Static,
              check_opts: [result: true, access_type: :filter],
              type: :authorize_if
            }
          ],
          bypass?: true,
          description: nil,
          access_type: :filter
        },
        %Ash.Policy.Policy{
          condition: [
            {Ash.Policy.Check.Action,
             [action: [:add_cookie], access_type: :filter]}
          ],
          policies: [
            %Ash.Policy.Check{
              check: {Ash.Policy.Check.ActorPresent, []},
              check_module: Ash.Policy.Check.ActorPresent,
              check_opts: [access_type: :filter],
              type: :authorize_if
            }
          ],
          bypass?: nil,
          description: nil,
          access_type: :filter
        },
        %Ash.Policy.Policy{
          condition: [
            {Ash.Policy.Check.Static, [result: true, access_type: :filter]}
          ],
          policies: [
            %Ash.Policy.Check{
              check: {Ash.Policy.Check.Static, [result: true]},
              check_module: Ash.Policy.Check.Static,
              check_opts: [result: true, access_type: :filter],
              type: :forbid_if
            }
          ],
          bypass?: nil,
          description: nil,
          access_type: :filter
        }
      ],
      resource: Bubblelog.Accounts.User,
      solver_statement: {:and,
       {:or, {Ash.Policy.Check.Static, [result: true, access_type: :filter]},
        {Ash.Policy.Check.Action, [action: [:add_cookie], access_type: :filter]}},
       {:and,
        {:or,
         {:not,
          {Ash.Policy.Check.Action,
           [action: [:add_cookie], access_type: :filter]}},
         {:and,
          {Ash.Policy.Check.Action,
           [action: [:add_cookie], access_type: :filter]},
          {Ash.Policy.Check.ActorPresent, [access_type: :filter]}}},
        {:or,
         {:and, {Ash.Policy.Check.Static, [result: true, access_type: :filter]},
          false},
         {:not, {Ash.Policy.Check.Static, [result: true, access_type: :filter]}}}}},
      domain: Bubblelog.Accounts,
      action: %Ash.Resource.Actions.Update{
        name: :add_cookie,
        primary?: false,
        description: "Add or update a cookie for the user",
        error_handler: nil,
        accept: [:cookie],
        require_attributes: [],
        allow_nil_input: [],
        skip_unknown_inputs: [],
        manual: nil,
        manual?: false,
        require_atomic?: false,
        atomic_upgrade?: false,
        atomic_upgrade_with: nil,
        action_select: nil,
        notifiers: [],
        atomics: [],
        delay_global_validations?: false,
        skip_global_validations?: false,
        arguments: [],
        changes: [],
        reject: [],
        metadata: [],
        transaction?: true,
        touches_resources: [],
        type: :update
      },
      changeset_doesnt_match_filter: false,
      splode: Ash.Error,
      bread_crumbs: ["Error returned from: Bubblelog.Accounts.User.add_cookie"],
      vars: [],
      path: [],
      stacktrace: #Splode.Stacktrace<>,
      class: :forbidden
    }
  ]
}

In the user resource I’ve got the following

# user.ex

actions do
  ...
  update :add_cookie do
    accept [:cookie]
  end
  ...
end

policies do
  ...
  policy action(:add_cookie) do
    authorize_if actor_present()
    authorize_if relates_to_actor_via(:id)
  end

  policy always() do
    forbid_if always()
  end
end

attributes do
  ...
  attribute :cookie, :string do
    sensitive? true
  end
end

if I comment out that policy always() the test passes – so I’m guessing the solution lives there somewhere?

full user.ex
defmodule Bubblelog.Accounts.User do
  use Ash.Resource,
    otp_app: :bubblelog,
    domain: Bubblelog.Accounts,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer],
    extensions: [AshAuthentication]

  authentication do
    add_ons do
      log_out_everywhere do
        apply_on_password_change? true
      end

      confirmation :confirm_new_user do
        monitor_fields [:email]
        confirm_on_create? true
        confirm_on_update? false
        require_interaction? true
        confirmed_at_field :confirmed_at
        auto_confirm_actions [:sign_in_with_magic_link, :reset_password_with_token]
        sender Bubblelog.Accounts.User.Senders.SendNewUserConfirmationEmail
      end
    end

    tokens do
      enabled? true
      token_resource Bubblelog.Accounts.Token
      signing_secret Bubblelog.Secrets
      store_all_tokens? true
      require_token_presence_for_authentication? true
    end

    strategies do
      password :password do
        identity_field :email
        hash_provider AshAuthentication.BcryptProvider

        resettable do
          sender Bubblelog.Accounts.User.Senders.SendPasswordResetEmail
          # these configurations will be the default in a future release
          password_reset_action_name :reset_password_with_token
          request_password_reset_action_name :request_password_reset_token
        end
      end

      magic_link do
        identity_field :email
        registration_enabled? true
        require_interaction? true

        sender Bubblelog.Accounts.User.Senders.SendMagicLinkEmail
      end
    end
  end

  postgres do
    table "users"
    repo Bubblelog.Repo
  end

  actions do
    defaults [:read]

    update :add_cookie do
      accept [:cookie]
      description "Add or update the Bubble cookie for the user"
    end

    read :get_by_subject do
      description "Get a user by the subject claim in a JWT"
      argument :subject, :string, allow_nil?: false
      get? true
      prepare AshAuthentication.Preparations.FilterBySubject
    end

    update :change_password do
      # Use this action to allow users to change their password by providing
      # their current password and a new password.

      require_atomic? false
      accept []
      argument :current_password, :string, sensitive?: true, allow_nil?: false

      argument :password, :string,
        sensitive?: true,
        allow_nil?: false,
        constraints: [min_length: 8]

      argument :password_confirmation, :string, sensitive?: true, allow_nil?: false

      validate confirm(:password, :password_confirmation)

      validate {AshAuthentication.Strategy.Password.PasswordValidation,
                strategy_name: :password, password_argument: :current_password}

      change {AshAuthentication.Strategy.Password.HashPasswordChange, strategy_name: :password}
    end

    read :sign_in_with_password do
      description "Attempt to sign in using a email and password."
      get? true

      argument :email, :ci_string do
        description "The email to use for retrieving the user."
        allow_nil? false
      end

      argument :password, :string do
        description "The password to check for the matching user."
        allow_nil? false
        sensitive? true
      end

      # validates the provided email and password and generates a token
      prepare AshAuthentication.Strategy.Password.SignInPreparation

      metadata :token, :string do
        description "A JWT that can be used to authenticate the user."
        allow_nil? false
      end
    end

    read :sign_in_with_token do
      # In the generated sign in components, we validate the
      # email and password directly in the LiveView
      # and generate a short-lived token that can be used to sign in over
      # a standard controller action, exchanging it for a standard token.
      # This action performs that exchange. If you do not use the generated
      # liveviews, you may remove this action, and set
      # `sign_in_tokens_enabled? false` in the password strategy.

      description "Attempt to sign in using a short-lived sign in token."
      get? true

      argument :token, :string do
        description "The short-lived sign in token."
        allow_nil? false
        sensitive? true
      end

      # validates the provided sign in token and generates a token
      prepare AshAuthentication.Strategy.Password.SignInWithTokenPreparation

      metadata :token, :string do
        description "A JWT that can be used to authenticate the user."
        allow_nil? false
      end
    end

    create :register_with_password do
      description "Register a new user with a email and password."

      argument :email, :ci_string do
        allow_nil? false
      end

      argument :password, :string do
        description "The proposed password for the user, in plain text."
        allow_nil? false
        constraints min_length: 8
        sensitive? true
      end

      argument :password_confirmation, :string do
        description "The proposed password for the user (again), in plain text."
        allow_nil? false
        sensitive? true
      end

      # Sets the email from the argument
      change set_attribute(:email, arg(:email))

      # Hashes the provided password
      change AshAuthentication.Strategy.Password.HashPasswordChange

      # Generates an authentication token for the user
      change AshAuthentication.GenerateTokenChange

      # validates that the password matches the confirmation
      validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation

      metadata :token, :string do
        description "A JWT that can be used to authenticate the user."
        allow_nil? false
      end
    end

    action :request_password_reset_token do
      description "Send password reset instructions to a user if they exist."

      argument :email, :ci_string do
        allow_nil? false
      end

      # creates a reset token and invokes the relevant senders
      run {AshAuthentication.Strategy.Password.RequestPasswordReset, action: :get_by_email}
    end

    read :get_by_email do
      description "Looks up a user by their email"
      get? true

      argument :email, :ci_string do
        allow_nil? false
      end

      filter expr(email == ^arg(:email))
    end

    update :reset_password_with_token do
      argument :reset_token, :string do
        allow_nil? false
        sensitive? true
      end

      argument :password, :string do
        description "The proposed password for the user, in plain text."
        allow_nil? false
        constraints min_length: 8
        sensitive? true
      end

      argument :password_confirmation, :string do
        description "The proposed password for the user (again), in plain text."
        allow_nil? false
        sensitive? true
      end

      # validates the provided reset token
      validate AshAuthentication.Strategy.Password.ResetTokenValidation

      # validates that the password matches the confirmation
      validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation

      # Hashes the provided password
      change AshAuthentication.Strategy.Password.HashPasswordChange

      # Generates an authentication token for the user
      change AshAuthentication.GenerateTokenChange
    end

    create :sign_in_with_magic_link do
      description "Sign in or register a user with magic link."

      argument :token, :string do
        description "The token from the magic link that was sent to the user"
        allow_nil? false
      end

      upsert? true
      upsert_identity :unique_email
      upsert_fields [:email]

      # Uses the information from the token to create or sign in the user
      change AshAuthentication.Strategy.MagicLink.SignInChange

      metadata :token, :string do
        allow_nil? false
      end
    end

    action :request_magic_link do
      argument :email, :ci_string do
        allow_nil? false
      end

      run AshAuthentication.Strategy.MagicLink.Request
    end
  end

  policies do
    bypass AshAuthentication.Checks.AshAuthenticationInteraction do
      authorize_if always()
    end

    policy action(:add_cookie) do
      authorize_if actor_present()
      authorize_if relates_to_actor_via(:id)
    end

    policy always() do
      forbid_if always()
    end
  end

  attributes do
    uuid_primary_key :id

    attribute :email, :ci_string do
      allow_nil? false
      public? true
    end

    attribute :hashed_password, :string do
      sensitive? true
    end

    attribute :cookie, :string do
      sensitive? true
    end

    attribute :confirmed_at, :utc_datetime_usec
  end

  identities do
    identity :unique_email, [:email]
  end
end

FYI

I generated a project with:

sh <(curl 'https://ash-hq.org/install/bubblelog?install=phoenix') \
    && cd bubblelog && mix igniter.install ash ash_phoenix \
    ash_json_api ash_postgres ash_authentication \
    ash_authentication_phoenix ash_admin ash_oban oban_web \
    live_debugger mishka_chelekom tidewave ash_ai \
    --auth-strategy password --auth-strategy magic_link --yes \
    && mix ash.setup

Marked As Solved

ken-kost

ken-kost

Why do you have this? It means ‘always apply - always forbid’ so it forbids everything.
I suppose you want to trigger the forbidden error if the action is not authorized. In that case, not super sure, but I think you can move forbid_if always() to the action policy. So that if the authorize_if checks are false then the forbid_if will be evaluated and forbid the action.

Also Liked

zachdaniel

zachdaniel

Creator of Ash

The default behavior is that everything is allowed, until you add the policy authorizer at which point nothing is allowed by default, for security reasons. If you add a blanket

policy always() do
  authorize_if always()
end

then any request w/ no other policies that apply will be authorized.

RicoTrevisan

RicoTrevisan

Thanks. It’s there because that’s what was generated when I first created the project. I think I got it now. I’ll remove it and go from there.

Where Next?

Popular in Questions Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New

Other popular topics Top

jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 126479 1222
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement