Created
October 11, 2023 05:07
-
-
Save ahey/0c81097f0ce23054a00c8b77a26a0ab5 to your computer and use it in GitHub Desktop.
Using ash and ash_authentication, configure a user resource for auth via GraphQL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule YourApp.User.Actions.ConfirmWithToken do | |
use Ash.Resource.ManualCreate | |
def create(changeset, _opts, _context) do | |
strategy = AshAuthentication.Info.strategy!(YourApp.User, :confirm) | |
AshAuthentication.Strategy.action( | |
strategy, | |
:confirm, | |
%{"confirm" => changeset.arguments[:token]} | |
) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule YourApp.User.Actions.ResetPasswordCreate do | |
use Ash.Resource.ManualCreate | |
@impl true | |
def create(changeset, _opts, _context) do | |
strategy = | |
AshAuthentication.Info.strategy!(YourApp.User, :password) | |
AshAuthentication.Strategy.Password.Actions.reset( | |
strategy, | |
changeset.arguments |> YourApp.Util.atom_to_string_keys(), | |
[] | |
) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule YourApp.User.Actions.SignIn do | |
use Ash.Resource.ManualCreate | |
def create(changeset, _opts, _context) do | |
result = | |
YourApp.User.sign_in_with_password_builtin( | |
changeset.arguments[:email], | |
changeset.arguments[:password] | |
) | |
with {:error, _} <- result do | |
{:error, login_failed_error(changeset)} | |
end | |
end | |
defp login_failed_error(changeset) do | |
changeset | |
|> Ash.Changeset.add_error([ | |
Ash.Error.Changes.InvalidArgument.exception(message: "login failed") | |
]) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule YourApp.User do | |
use Ash.Resource, | |
data_layer: AshPostgres.DataLayer, | |
authorizers: [Ash.Policy.Authorizer], | |
extensions: [ | |
AshAuthentication, | |
AshGraphql.Resource | |
] | |
postgres do | |
table "users" | |
repo YourApp.Repo | |
end | |
identities do | |
identity :unique_email, [:email], eager_check_with: YourApp.Api | |
end | |
attributes do | |
uuid_primary_key :id | |
attribute :email, :ci_string, allow_nil?: false | |
attribute :hashed_password, | |
:string, | |
allow_nil?: true, | |
sensitive?: true, | |
private?: true | |
create_timestamp :inserted_at | |
update_timestamp :updated_at | |
end | |
code_interface do | |
define_for YourApp.Api | |
define :confirm, args: [:confirm] | |
define :change_password, args: [:current_password, :password] | |
define :change_email, args: [:current_password, :email] | |
define :confirm_without_token | |
define :confirm_with_token, args: [:token] | |
define :register_with_password, args: [:email, :password] | |
define :register_preconfirmed, args: [:email, :password] | |
define :sign_in_with_password_builtin, args: [:email, :password] | |
define :sign_in_with_password, args: [:email, :password] | |
define :request_password_reset, | |
action: :request_password_reset_with_password, | |
args: [:email] | |
define :reset_password, args: [:reset_token, :password] | |
end | |
actions do | |
defaults [:create, :read, :update, :destroy] | |
update :confirm_without_token do | |
accept [] | |
change set_attribute(:confirmed_at, &DateTime.utc_now/0) | |
end | |
create :confirm_with_token do | |
accept [] | |
argument :token, :string, sensitive?: true, allow_nil?: false | |
manual YourApp.User.Actions.ConfirmWithToken | |
end | |
# The sign_in action provided by ash_authentication is a read action. We | |
# need it as a create action so that the errors can be returned via GraphQL | |
create :sign_in_with_password do | |
accept [] | |
argument :email, :string, allow_nil?: false | |
argument :password, :string, sensitive?: true, allow_nil?: false | |
metadata :token, :string, allow_nil?: false | |
manual YourApp.User.Actions.SignIn | |
end | |
# Confirm the credential immediately without sending confirmation email | |
create :register_preconfirmed do | |
accept [:email] | |
argument :password, :string, sensitive?: true, allow_nil?: false | |
metadata :token, :string, allow_nil?: false | |
allow_nil_input [:hashed_password] | |
change {AshAuthentication.Strategy.Password.HashPasswordChange, | |
strategy_name: :password} | |
change {AshAuthentication.GenerateTokenChange, strategy_name: :password} | |
change set_attribute(:confirmed_at, &DateTime.utc_now/0) | |
end | |
update :change_password do | |
accept [] | |
argument :current_password, :string, sensitive?: true, allow_nil?: false | |
argument :password, :string, sensitive?: true, allow_nil?: false | |
validate {AshAuthentication.Strategy.Password.PasswordValidation, | |
strategy_name: :password, password_argument: :current_password} | |
change {AshAuthentication.Strategy.Password.HashPasswordChange, | |
strategy_name: :password} | |
end | |
update :change_email do | |
accept [] | |
argument :current_password, :string, sensitive?: true, allow_nil?: false | |
argument :email, :string, allow_nil?: false | |
validate {AshAuthentication.Strategy.Password.PasswordValidation, | |
strategy_name: :password, password_argument: :current_password} | |
change set_attribute(:email, arg(:email)) | |
end | |
# Had to create this action, as the one provided by ash_authentication | |
# requires the actor to be provided as input, strangely. | |
# See https://github.com/team-alembic/ash_authentication/issues/207 | |
create :reset_password do | |
accept [] | |
argument :reset_token, :string, allow_nil?: false | |
argument :password, :string, sensitive?: true, allow_nil?: false | |
manual YourApp.User.Actions.ResetPasswordCreate | |
end | |
end | |
graphql do | |
type :user | |
queries do | |
get :user_request_password_reset, | |
:request_password_reset_with_password do | |
identity false | |
as_mutation? true | |
end | |
end | |
mutations do | |
create :user_register, :register_with_password | |
update :user_change_email, :change_email | |
update :user_change_password, :change_password | |
create :user_reset_password, :reset_password | |
create :user_confirm_with_token, :confirm_with_token | |
create :user_sign_in, :sign_in_with_password | |
end | |
end | |
policies do | |
bypass AshAuthentication.Checks.AshAuthenticationInteraction do | |
authorize_if always() | |
end | |
bypass action(:sign_in_with_password) do | |
authorize_if always() | |
end | |
bypass action(:sign_in_with_password_builtin) do | |
authorize_if always() | |
end | |
bypass action(:register_with_password) do | |
authorize_if always() | |
end | |
bypass action(:confirm_with_token) do | |
authorize_if always() | |
end | |
bypass action(:request_password_reset_with_password) do | |
authorize_if always() | |
end | |
bypass action(:reset_password) do | |
authorize_if always() | |
end | |
policy always() do | |
forbid_if always() | |
end | |
end | |
authentication do | |
api YourApp.Api | |
add_ons do | |
confirmation :confirm do | |
monitor_fields [:email] | |
sender fn credential, token, opts -> | |
changeset = Keyword.fetch!(opts, :changeset) | |
email = changeset.attributes[:email] |> Ash.CiString.value() | |
case changeset.action.name do | |
:register_with_password -> | |
YourApp.Mailer.send_confirm_email_instructions(credential, email, token) | |
:change_email -> | |
YourApp.Mailer.send_update_email_instructions(credential, email, token) | |
_ -> | |
nil | |
end | |
end | |
end | |
end | |
strategies do | |
password do | |
identity_field :email | |
sign_in_action_name :sign_in_with_password_builtin | |
confirmation_required? false | |
resettable do | |
sender fn credential, token, opts -> | |
email = credential.email |> Ash.CiString.value() | |
YourApp.Mailer.send_reset_password_instructions(credential, email, token) | |
end | |
end | |
end | |
end | |
tokens do | |
enabled? true | |
require_token_presence_for_authentication? true | |
signing_secret &YourApp.Auth.get_config/2 | |
store_all_tokens? true | |
token_resource YourApp.AuthToken | |
token_lifetime YourApp.session_max_duration() |> YourApp.Util.to_hours() | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment