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
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
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
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.
Popular in Questions
Other popular topics
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
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








