# AshAuthentication Usage Rules ## Core Concepts - **Strategies**: password, OAuth2, magic_link, api_key authentication methods - **Tokens**: JWT for stateless authentication - **UserIdentity**: links users to OAuth2 providers - **Add-ons**: confirmation, logout-everywhere functionality - **Actions**: auto-generated by strategies (register, sign_in, etc.), can be overridden on the resource ## Key Principles - Always use secrets management - never hardcode credentials - Enable tokens for magic_link, confirmation, OAuth2 - UserIdentity resource optional for OAuth2 (required for multiple providers per user) - API keys require strict policy controls and expiration management - Use prefixes for API keys to enable secret scanning compliance - Check existing strategies: `AshAuthentication.Info.strategies/1` ## Strategy Selection **Password** - Email/password authentication - Requires: `:email`, `:hashed_password` attributes, unique identity **Magic Link** - Passwordless email authentication - Requires: `:email` attribute, sender implementation, tokens enabled **API Key** - Token-based authentication for APIs - Requires: API key resource, relationship to user, sign-in action **OAuth2** - Social/enterprise login (GitHub, Google, Auth0, Apple, OIDC, Slack) - Requires: custom actions, secrets - Optional: UserIdentity resource (for multiple providers per user) ## Password Strategy ```elixir authentication do strategies do password :password do identity_field :email hashed_password_field :hashed_password resettable do sender MyApp.PasswordResetSender end end end end # Required attributes: attributes do attribute :email, :ci_string, allow_nil?: false, public?: true attribute :hashed_password, :string, allow_nil?: false, sensitive?: true end identities do identity :unique_email, [:email] end ``` ## Magic Link Strategy ```elixir authentication do strategies do magic_link do identity_field :email sender MyApp.MagicLinkSender end end end # Sender implementation required: defmodule MyApp.MagicLinkSender do use AshAuthentication.Sender def send(user_or_email, token, _opts) do MyApp.Emails.deliver_magic_link(user_or_email, token) end end ``` ## API Key Strategy ```elixir # 1. Create API key resource defmodule MyApp.Accounts.ApiKey do use Ash.Resource, data_layer: AshPostgres.DataLayer, authorizers: [Ash.Policy.Authorizer] actions do defaults [:read, :destroy] create :create do primary? true accept [:user_id, :expires_at] change {AshAuthentication.Strategy.ApiKey.GenerateApiKey, prefix: :myapp, hash: :api_key_hash} end end attributes do uuid_primary_key :id attribute :api_key_hash, :binary, allow_nil?: false, sensitive?: true attribute :expires_at, :utc_datetime_usec, allow_nil?: false end relationships do belongs_to :user, MyApp.Accounts.User, allow_nil?: false end calculations do calculate :valid, :boolean, expr(expires_at > now()) end identities do identity :unique_api_key, [:api_key_hash] end policies do bypass AshAuthentication.Checks.AshAuthenticationInteraction do authorize_if always() end end end # 2. Add strategy to user resource authentication do strategies do api_key do api_key_relationship :valid_api_keys api_key_hash_attribute :api_key_hash end end end # 3. Add relationship to user relationships do has_many :valid_api_keys, MyApp.Accounts.ApiKey do filter expr(valid) end end # 4. Add sign-in action to user actions do read :sign_in_with_api_key do argument :api_key, :string, allow_nil?: false prepare AshAuthentication.Strategy.ApiKey.SignInPreparation end end ``` **Security considerations:** - API keys are hashed for storage security - Use policies to restrict API key access to specific actions - Check `user.__metadata__[:using_api_key?]` to detect API key authentication - Access the API key via `user.__metadata__[:api_key]` for permission checks ## OAuth2 Strategies **Supported providers:** github, google, auth0, apple, oidc, slack **Required for all OAuth2:** - Custom `register_with_[provider]` action - Secrets management - Tokens enabled **Optional for all OAuth2:** - UserIdentity resource (for multiple providers per user) ### OAuth2 Configuration Pattern ```elixir # Strategy configuration authentication do strategies do github do # or google, auth0, apple, oidc, slack client_id MyApp.Secrets client_secret MyApp.Secrets redirect_uri MyApp.Secrets # auth0 also needs: base_url # apple also needs: team_id, private_key_id, private_key_path # oidc also needs: openid_configuration_uri identity_resource MyApp.Accounts.UserIdentity end end end # Required action (replace 'github' with provider name) actions do create :register_with_github do argument :user_info, :map, allow_nil?: false argument :oauth_tokens, :map, allow_nil?: false upsert? true upsert_identity :unique_email change AshAuthentication.GenerateTokenChange # If UserIdentity resource is being used change AshAuthentication.Strategy.OAuth2.IdentityChange change fn changeset, _ctx -> user_info = Ash.Changeset.get_argument(changeset, :user_info) Ash.Changeset.change_attributes(changeset, Map.take(user_info, ["email"])) end end end ``` ## Add-ons ### Confirmation ```elixir authentication do tokens do enabled? true token_resource MyApp.Accounts.Token end add_ons do confirmation :confirm do monitor_fields [:email] sender MyApp.ConfirmationSender end end end ``` ### Log Out Everywhere ```elixir authentication do tokens do store_all_tokens? true end add_ons do log_out_everywhere do apply_on_password_change? true end end end ``` ## Working with Authentication ### Strategy Protocol ```elixir # Get and use strategies strategy = AshAuthentication.Info.strategy!(MyApp.User, :password) {:ok, user} = AshAuthentication.Strategy.action(strategy, :sign_in, params) # List strategies strategies = AshAuthentication.Info.strategies(MyApp.User) ``` ### Token Operations ```elixir # User/subject conversion subject = AshAuthentication.user_to_subject(user) {:ok, user} = AshAuthentication.subject_to_user(subject, MyApp.User) # Token management AshAuthentication.TokenResource.revoke(MyApp.Token, token) ``` ### Policies ```elixir policies do bypass AshAuthentication.Checks.AshAuthenticationInteraction do authorize_if always() end end ``` ## Common Implementation Patterns ### Pattern: Multiple Authentication Methods When users need multiple ways to authenticate: ```elixir authentication do tokens do enabled? true token_resource MyApp.Accounts.Token end strategies do password :password do identity_field :email hashed_password_field :hashed_password end github do client_id MyApp.Secrets client_secret MyApp.Secrets redirect_uri MyApp.Secrets identity_resource MyApp.Accounts.UserIdentity end magic_link do identity_field :email sender MyApp.MagicLinkSender end end end ``` ### Pattern: OAuth2 with User Registration When new users can register via OAuth2: ```elixir actions do create :register_with_github do argument :user_info, :map, allow_nil?: false argument :oauth_tokens, :map, allow_nil?: false upsert? true upsert_identity :email change AshAuthentication.GenerateTokenChange change fn changeset, _ctx -> user_info = Ash.Changeset.get_argument(changeset, :user_info) changeset |> Ash.Changeset.change_attribute(:email, user_info["email"]) |> Ash.Changeset.change_attribute(:name, user_info["name"]) end end end ``` ### Pattern: Custom Token Configuration When you need specific token behavior: ```elixir authentication do tokens do enabled? true token_resource MyApp.Accounts.Token signing_secret MyApp.Secrets token_lifetime {24, :hours} store_all_tokens? true # For logout-everywhere functionality require_token_presence_for_authentication? false end end ``` ## Customizing Authentication Actions When customizing generated authentication actions (register, sign_in, etc.): **Key Security Rules:** - Always mark credentials with `sensitive?: true` (passwords, API keys, tokens) - Use `public?: false` for internal fields and highly sensitive PII - Use `public?: true` for identity fields and UI display data - Include required authentication changes (`GenerateTokenChange`, `HashPasswordChange`, etc.) **Argument Handling:** - All arguments must be used in `accept` or `change set_attribute()` - Use `allow_nil?: false` for required arguments - OAuth2 data must be extracted in changes, not accepted directly **Example Custom Registration:** ```elixir create :register_with_password do argument :password, :string, allow_nil?: false, sensitive?: true argument :first_name, :string, allow_nil?: false accept [:email, :first_name] change AshAuthentication.GenerateTokenChange change AshAuthentication.Strategy.Password.HashPasswordChange end ``` For more guidance, see the "Customizing Authentication Actions" section in the getting started guide.