Packages
ash_authentication
4.14.1
5.0.0-rc.12
5.0.0-rc.11
5.0.0-rc.10
5.0.0-rc.9
5.0.0-rc.8
5.0.0-rc.7
5.0.0-rc.6
5.0.0-rc.5
5.0.0-rc.4
5.0.0-rc.3
5.0.0-rc.2
5.0.0-rc.1
5.0.0-rc.0
4.14.1
4.14.0
4.13.7
4.13.6
4.13.5
4.13.4
4.13.3
4.13.2
retired
4.13.1
retired
4.13.0
4.12.0
4.11.0
4.10.0
4.9.9
4.9.8
4.9.7
4.9.6
4.9.5
4.9.4
4.9.3
4.9.2
4.9.1
4.9.0
4.8.7
4.8.6
4.8.5
4.8.3
4.8.2
4.8.1
4.8.0
4.7.6
4.7.5
4.7.4
4.7.3
4.7.2
4.7.1
4.7.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.6
4.5.5
4.5.4
4.5.3
4.5.2
4.5.1
4.5.0
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.12
4.3.11
4.3.10
4.3.9
4.3.8
4.3.7
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.4
4.0.3
4.0.2
4.0.1
4.0.0
4.0.0-rc.6
4.0.0-rc.5
4.0.0-rc.3
4.0.0-rc.2
4.0.0-rc.1
4.0.0-rc.0
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.16
3.11.15
3.11.14
3.11.13
3.11.12
3.11.11
3.11.10
3.11.9
3.11.8
3.11.7
3.11.6
3.11.5
3.11.4
3.11.3
3.11.2
3.11.1
3.11.0
3.10.8
3.10.7
3.10.6
3.10.5
3.10.4
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.0
3.7.9
3.7.8
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
3.6.1
3.6.0
3.5.3
3.5.2
3.5.1
3.5.0
3.3.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.0
3.0.3
Authentication extension for the Ash Framework.
Current section
Files
Jump to
Current section
Files
ash_authentication
usage-rules.md
usage-rules.md
<!--
SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
SPDX-License-Identifier: MIT
-->
# 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 required for all OAuth2/OIDC strategies (stores the provider's `iss`/`sub` claims; matching users by email is unsafe)
- 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, UserIdentity resource
## 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
- UserIdentity resource (stores the provider's `iss`/`sub` identity claims - matching users by email or other claims is unsafe)
**How users are matched (sign-in and registration):**
- Users are resolved by the `(strategy, sub)` identity, never by email. A provider identity belongs to exactly one local user, permanently.
- A `sub` that has been seen before signs that user in.
- A new `sub` whose email matches an existing account is only linked automatically when the provider's `email_verified` claim is trusted (`trust_email_verified?`). Otherwise the behaviour depends on `on_untrusted_email_match`.
- `trust_email_verified?` defaults to `true` for GitHub/Google/Auth0/Slack/Apple and `false` elsewhere. Only enable it for providers that reliably assert email ownership.
- `on_untrusted_email_match` controls the untrusted-email-matches-existing-account case: `:reject` (the default) refuses the sign-in (the user must sign in with their existing method to link the provider); `:confirm` issues a confirmation to the existing account's email and links the provider only once the recipient proves ownership by confirming. `:confirm` requires a `confirmation` add-on (enforced at compile time).
- **Security note for `:confirm`**: confirming binds whatever provider identity initiated the flow, so the confirmation email must make clear *which* provider is being linked and that confirming grants it access — otherwise a user can be social-engineered into linking an attacker's provider account. The generated confirmation sender branches on `opts[:confirmation_type] == :identity_link` to produce this copy.
### 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
# Required: persists the provider's `iss`/`sub` identity claims
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
# Required: persists the provider's `iss`/`sub` identity claims
change AshAuthentication.Strategy.OAuth2.IdentityChange
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.