setup usage_rules
This commit is contained in:
372
.claude/skills/ash-framework/references/ash_authentication.md
Normal file
372
.claude/skills/ash-framework/references/ash_authentication.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user