Ash Authentication

repository·main·Indexed 41 days ago

https://github.com/team-alembic/ash_authentication

A robust, extensible authentication framework for the Ash ecosystem. It supports multiple strategies including passwords, OAuth2 (Google, GitHub, Auth0, Slack), magic links, API keys, and TOTP, along with add-ons for audit logging, email confirmation, and global session logout.

Tokens
87.8K
Snippets
204
Records
297
Agent score
46%

What's inside Ash Authentication

  1. Overview of Ash Authentication

    main

    Ash Authentication is a library designed to provide authentication capabilities for applications built with the Ash Framework. It supports various authentication strategies including Passwords, OAuth2 (Google, GitHub, Auth0, etc.), Magic Links, API Keys, and TOTP.

    To integrate Ash Authentication into a Phoenix application, you should use the companion package ash_authentication_phoenix.

  2. What is AshAuthentication.UserIdentity and when to use it

    main

    The AshAuthentication.UserIdentity extension generates a dedicated user identities resource. This resource is essential if you want to support multiple authentication strategies simultaneously (e.g., allowing a user to sign in via both Google OAuth2 and a local password, or managing multiple remote identities for a single user).

    Key benefits include:

    1. Multi-strategy support: A single local user can be linked to multiple remote authentication providers.
    2. Token Management: It enables automatic refreshing of tokens for providers that support it by storing remote identity information (like access and refresh tokens) and mapping them to your local user records.
  3. Configure email trust and matching for OAuth2

    main

    When using OAuth2, you must decide how to handle email matching and trust:

    • trust_email_verified?: If true, the strategy trusts the provider's email_verified claim to automatically link an OAuth2 sign-in to an existing local account with the same email. This should only be enabled for providers that reliably assert email ownership.
    • email_field: The attribute on your user resource that holds the email address (defaults to :email).
    • on_untrusted_email_match: Defines behavior when a new provider identity presents an email matching an existing account, but trust_email_verified? is false:
      • :reject (Default): Refuses the sign-in.
      • :confirm: Issues a confirmation to the existing account's email to link the provider once ownership is proven. Requires a confirmation add-on.
  4. Configure Brute Force Protection for Recovery Codes

    main

    Recovery codes require a brute force protection strategy to prevent automated guessing. You can choose from three options:

    1. Audit Log (recommended): Tracks failed attempts in the audit log. Requires an audit log add-on. You can configure the audit_log_window and audit_log_max_failures.
    2. Rate Limiting: Uses AshRateLimiter. Requires the AshRateLimiter extension.
    3. Custom Preparation: Allows you to implement custom logic by providing a module that implements supports/1 (returning a list including Ash.ActionInput).

    Example using Audit Log with custom thresholds:

    recovery_code do
      recovery_code_resource MyApp.Accounts.RecoveryCode
      brute_force_strategy {:audit_log, :audit_log}
      audit_log_window {5, :minutes}
      audit_log_max_failures 5
    end
  5. How multitenancy works with OidcConnection

    main

    The OidcConnection extension supports multitenancy automatically.

    • Multitenant resources: If your resource is multitenant, the AshAuthentication.Strategy.DynamicOidc strategy will automatically scope connection lookups by the current Ash tenant.
    • Single-tenant resources: If the resource is not multitenant, the strategy will look up connections globally. This is useful for single-tenant deployments that need to support multiple Identity Providers (IdPs).
  6. Understand WebAuthn strategy modes: Primary vs Second Factor

    main

    The WebAuthn strategy can operate in two distinct modes by configuring the registration_enabled?, sign_in_enabled?, and verify_enabled? flags:

    1. Primary (Default): All three flags are true. Users use passkeys as their main credential to register and sign in directly.
    2. Second factor (2FA): Set registration_enabled? false, sign_in_enabled? false, and verify_enabled? true. Passkeys are used only to verify an assertion for an already authenticated user. Upon success, a webauthn_verified_at claim is added to the user's authentication token.
  7. Configure TOTP Brute Force Protection strategies

    main

    TOTP requires a brute force protection strategy to prevent code guessing. You can choose from three main approaches:

    1. Custom Preparation: Implement your own logic (rate limiting, lockout) by creating a module that uses Ash.Resource.Preparation.
    2. Rate Limiting: Use the :rate_limit atom (requires AshRateLimiter extension).
    3. Audit Log: Use {:audit_log, :my_audit_log} (requires an audit log add-on).

    Example of a Custom Preparation module:

    defmodule MyApp.TotpBruteForcePreparation do
      use Ash.Resource.Preparation
    
      def prepare(query, _opts, _context) do
        # Implement rate limiting, account lockout, etc.
        query
      end
    end
    brute_force_strategy {:preparation, MyApp.TotpBruteForcePreparation}
  8. Enable Sign-in Tokens

    main

    Sign-in tokens are short-lived tokens that can be generated to facilitate specific flows, such as exchanging them for a real token via the validate_sign_in_token route. This is commonly used by ash_authentication_phoenix to support LiveView sign-ins that redirect to controller actions.

    To enable this, use the following configuration in your password strategy: d:AshAuthentication.Strategy.Password.authentication.strategies.password.sign_in_tokens_enabled?.

    To generate one, set the :token_type context to :sign_in when calling the sign-in action.

  9. How recovery codes work in Ash Authentication

    main

    Recovery codes are a single-use authentication mechanism. The system generates codes, displays them to the user once, and stores only their hashed values.

    The authentication lifecycle:

    1. The user submits a plaintext code.
    2. The system hashes the submitted code.
    3. The hash is matched against stored values in the database.
    4. Upon a successful match, the specific code record is deleted to enforce single-use.
    5. The user is authenticated.

    Security Note: Plaintext codes are never stored in the database; they only exist in memory during the generation phase and are returned to the caller via action metadata.

  10. Use `require_interaction?` to prevent token consumption by bots

    main

    By default, some email clients or virus scanners may automatically fetch links, which consumes the magic link token and causes the user's subsequent click to fail.

    Setting require_interaction? true mitigates this by serving a simple form (a GET request) that requires the user to click a "sign in" button (a POST request) to actually consume the token.

    Note: This serves a simple form at the magic link endpoint. In production, you may want to place your own custom page at that path in your router or change the email link to a different URL. See AshAuthentication.Phoenix.Router.magic_sign_in_route/3 for details.

  11. Understand recovery code verification mechanics

    main

    The verification process differs based on whether the hash provider is deterministic or non-deterministic.

    Deterministic Verification (e.g., SHA-256)

    Uses a single atomic database operation to prevent race conditions:

    1. Hash the submitted code.
    2. Execute: DELETE FROM recovery_codes WHERE user_id = ? AND code = "<hashed_value>"
    3. If a row was deleted, the code was valid.

    Non-Deterministic Verification (e.g., Bcrypt)

    Requires manual iteration and locking:

    1. Execute SELECT * FROM recovery_codes WHERE user_id = ? FOR UPDATE to lock the rows.
    2. Iterate through each stored hash and perform bcrypt_verify(submitted_code, stored_hash).
    3. If a match is found, DELETE FROM recovery_codes WHERE id = ?.
    4. The system pads remaining iterations with simulate() to ensure constant-time execution and prevent timing attacks that could leak how many codes remain.