# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd # # SPDX-License-Identifier: MIT defmodule AshAuthentication.Strategy.RememberMe do alias __MODULE__.Dsl alias AshAuthentication.{ Strategy.Custom, Strategy.RememberMe.Transformer, Strategy.RememberMe.Verifier } @moduledoc """ Strategy for authenticating using a remember me token that has a configurable token_lifetime and is typically valid longer than a session token. Remember me tokens are generated by other strategies (e.g. MagicLink) to allow for authentication to continue beyond the scope of the current session. In order to use remember me authentication you need to have another strategy enabled that supports remember me. ### Example Usage Add the Remember Me strategy to your authenticated resource and update another strategy to the generate the remember_me token. ```elixir defmodule MyApp.Accounts.User do use Ash.Resource, extensions: [AshAuthentication], domain: MyApp.Accounts attributes do uuid_primary_key :id attribute :email, :ci_string, allow_nil?: false end authentication do # Tokens are required for RememberMe tokens do enabled? true store_all_tokens? true require_token_presence_for_authentication? true end # Make sure you use the log_out_everywhere add on # to revoke remember me tokens on password change otherwise # the remember me tokens will continue to sign in users add_ons do log_out_everywhere do apply_on_password_change? true end end strategies do password do identity_field :email hashed_password_field :hashed_password hash_provider AshAuthentication.BcryptProvider confirmation_required? true end # Add the remember me Strategy remember_me :remember_me do sign_in_action_name :sign_in_with_remember_me # Optional defaults to :sign_in_with_[:strategy_name] cookie_name :remember_me # Optional. Defaults to :remember_me token_lifetime {30, :days} # Optional. Defaults to {30, :days} end end end # In any of the actions used by your other strategies... actions do read :sign_in_with_password do ... # Add an argument to your form argument :remember_me, :boolean do description "Whether to generate a remember me token." allow_nil? true end # Add the preparation that generates the token prepare {AshAuthentication.Strategy.RememberMe.MaybeGenerateTokenPreparation, strategy_name: :remember_me} # Add the metadata map that will contain the cookie_name, token, and other values # after a successful sign in. metadata :remember_me, :map do description "A map that includes the token options" allow_nil? true end end read :sign_in_with_remember_me do description "Attempt to sign in using a remember me token." get? true argument :token, :string do description "The remember me token" allow_nil? false sensitive? true end # validates the provided the remember me token and generates a token for the session prepare AshAuthentication.Strategy.RememberMe.SignInPreparation metadata :token, :string do description "A JWT that can be used to authenticate the user." allow_nil? false end end end end ``` When the user successfully signs in, the user.__metadata__.remember_me field will contain a map with the token and max_age. For `create` actions, such as registering a new user and signing them in immediately, use `AshAuthentication.Strategy.RememberMe.MaybeGenerateTokenChange`. This serves as an alternative to the preparation used for read actions and mirrors the behavior of the Magic Link strategy. If you're using AshAuthentication.Phoenix, update your AuthController to store the cookie with a `put_remember_me_cookie/3` callback. Other callbacks include delete_remember_me_cookie/2, and delete_all_remember_me_cookies/1 ```elixir defmodule MyAppWeb.AuthController do use MyAppWeb, :controller use AshAuthentication.Phoenix.Controller @impl AshAuthentication.Phoenix.Controller def put_remember_me_cookie(conn, cookie_name, %{token: token, max_age: max_age}) do cookie_options = [ max_age: max_age, # matches the token lifetime http_only: true, # prevents the cookie from being accessed by JavaScript secure: true, # only send the cookie over HTTPS same_site: "lax" # prevents the cookie from being sent with cross-site requests ] conn |> put_resp_cookie(cookie_name, token, cookie_options) end end ``` Update your router to sign in the user with the remember me token. ```elixir defmodule MyAppWeb.Router do pipeline :browser do ... plug :sign_in_with_remember_me # make sure this comes before load_from_session plug :load_from_session end end ``` You should also delete the remember me token on sign out to prevent the user from immediately being signed back in. This will sign them out of their current session, but it will not revoke remember me tokens on other devices. ```elixir defmodule MyAppWeb.AuthController do use MyAppWeb, :controller use AshAuthentication.Phoenix.Controller @impl AshAuthentication.Phoenix.Controller def sign_out(conn, _params) do return_to = get_session(conn, :return_to) || ~p"/" conn |> clear_session(:my_otp_app) |> AshAuthentication.Strategy.RememberMe.Plug.Helpers.delete_all_remember_me_cookies(:my_otp_app) |> put_flash(:info, "You are now signed out") |> redirect(to: return_to) end end ``` """ defstruct identity_field: :username, cookie_name: :remember_me, name: nil, registration_enabled?: false, resource: nil, token_lifetime: {30, :days}, sign_in_action_name: nil, __spark_metadata__: nil use Custom, entity: Dsl.dsl() defdelegate transform(strategy, dsl_state), to: Transformer defdelegate verify(strategy, dsl_state), to: Verifier @type t :: %__MODULE__{ identity_field: atom, name: atom, registration_enabled?: boolean, resource: module, identity_field: atom, cookie_name: atom, sign_in_action_name: :atom, token_lifetime: pos_integer, __spark_metadata__: Spark.Dsl.Entity.spark_meta() } end