Functions and schema for persistent user tokens used by sessions, magic links, email confirmation (a link and a code) and email-change workflows.
Tokens generated by this module are stored hashed when sent via email and stored raw for session tokens (which are signed). The module provides helper queries for verification and convenient builders used throughout the app.
Summary
Functions
A confirmation code for user: six digits a player types into the game
instead of opening the emailed link. Returns {code, user_token}.
Builds a token and its hash to be delivered to the user's email.
Generates a token that will be stored in a signed place, such as session or cookie. As they are signed, those tokens do not need to be hashed.
How long an email-change link stays valid, in days (auth.change_email_days).
How long an email confirmation link stays valid, in days (auth.confirm_email_days).
Query selecting token rows that are past their own context's validity window.
How long a magic link stays valid, in minutes (auth.magic_link_minutes, at most 60).
How long a browser session lasts, in days (auth.session_days).
Checks if the token is valid and returns its underlying lookup query.
The token rows matching code for user: sent to the address the account
has now, and younger than confirm_validity_in_days/0.
Checks if the token is valid and returns its underlying lookup query.
Checks if the token is valid and returns its underlying lookup query.
Types
@type t() :: %Gamend.Accounts.UserToken{ __meta__: term(), authenticated_at: DateTime.t() | nil, context: String.t() | nil, id: Ecto.UUID.t() | nil, inserted_at: DateTime.t() | nil, sent_to: String.t() | nil, token: binary() | nil, user: Gamend.Accounts.User.t() | Ecto.Association.NotLoaded.t() | nil, user_id: Ecto.UUID.t() | nil }
Functions
@spec build_confirm_code_token(Gamend.Accounts.User.t()) :: {String.t(), t()}
A confirmation code for user: six digits a player types into the game
instead of opening the emailed link. Returns {code, user_token}.
Only a hash is stored, salted with the user id: six digits repeat across
accounts, and the [context, token] index is unique.
Builds a token and its hash to be delivered to the user's email.
The non-hashed token is sent to the user email while the hashed part is stored in the database. The original token cannot be reconstructed, which means anyone with read-only access to the database cannot directly use the token in the application to gain access. Furthermore, if the user changes their email in the system, the tokens sent to the previous email are no longer valid.
Users can easily adapt the existing code to provide other types of delivery methods, for example, by phone numbers.
Generates a token that will be stored in a signed place, such as session or cookie. As they are signed, those tokens do not need to be hashed.
The reason why we store session tokens in the database, even though Phoenix already provides a session cookie, is because Phoenix' default session cookies are not persisted, they are simply signed and potentially encrypted. This means they are valid indefinitely, unless you change the signing/encryption salt.
Therefore, storing them allows individual user sessions to be expired. The token system can also be extended to store additional data, such as the device used for logging in. You could then use this information to display all valid sessions and devices in the UI and allow users to explicitly expire any session they deem invalid.
@spec change_email_validity_in_days() :: pos_integer()
How long an email-change link stays valid, in days (auth.change_email_days).
@spec confirm_validity_in_days() :: pos_integer()
How long an email confirmation link stays valid, in days (auth.confirm_email_days).
@spec expired_query() :: Ecto.Query.t()
Query selecting token rows that are past their own context's validity window.
Each context expires on a different clock (by default session 14d, magic link 15min, email change and confirmation, link and code, 7d), and those windows live here — so retention inverts the same predicate the verify queries use instead of guessing a single age. Contexts this module does not know are never selected.
@spec magic_link_validity_in_minutes() :: pos_integer()
How long a magic link stays valid, in minutes (auth.magic_link_minutes, at most 60).
@spec session_validity_in_days() :: pos_integer()
How long a browser session lasts, in days (auth.session_days).
Checks if the token is valid and returns its underlying lookup query.
The query returns the user_token found by the token, if any.
This is used to validate requests to change the user
email.
The given token is valid if it matches its hashed counterpart in the
database and if it has not expired (after change_email_validity_in_days/0).
The context must always start with "change:".
@spec verify_confirm_code_query(Gamend.Accounts.User.t(), String.t()) :: Ecto.Query.t()
The token rows matching code for user: sent to the address the account
has now, and younger than confirm_validity_in_days/0.
Checks if the token is valid and returns its underlying lookup query.
If found, the query returns a tuple of the form {user, token}.
The given token is valid if it matches its hashed counterpart in the
database. This function also checks if the token is being used within
magic_link_validity_in_minutes/0. The context of a magic link token is
always "login".
Checks if the token is valid and returns its underlying lookup query.
The query returns the user found by the token, if any, along with the token's creation time.
The token is valid if it matches the value in the database and it has
not expired (after session_validity_in_days/0).