View Source Plausible.Auth.TOTP (Plausible v0.0.1)

TOTP auth context

Handles all the aspects of TOTP setup, management and validation for users.

Setup

TOTP setup is started with initiate/1. At this stage, a random secret binary is generated for user and stored under User.totp_secret. The secret is additionally encrypted while stored in the database using Cloak. The vault for safe storage is configured in Plausible.Auth.TOTP.Vault via a dedicated Ecto type defined in Plausible.Auth.TOTP.EncryptedBinary. The function returns updated user along with TOTP URI and a readable form of secret. Both - the URI and readable secret - are meant for exposure in the user's setup screen. The URI should be encoded as a QR code.

After initiation, user is expected to confirm valid setup with enable/2, providing TOTP code from their authenticator app. After code validation passes successfully, the User.totp_enabled flag is set to true. Finally, the user must be immediately presented with a list of recovery codes returned by the same call of enable/2. The codes should be presented in copy/paste friendly form, ideally also with a print-friendly view option.

The initiate/1 and enable/1 functions can be safely called multiple times, allowing user to abort and restart setup up to these stages.

Management

The state of TOTP for a particular user can be chcecked by calling enabled?/1 or initiated?/1.

TOTP can be disabled with disable/2. User is expected to provide their current password for safety. Once disabled, all TOTP user settings are cleared and any remaining generated recovery codes are removed. The function can be safely run more than once.

If the user needs to regenerate the recovery codes outside of setup procedure, they must do it via generate_recovery_codes/2, providing their current password for safety. They must be warned that any existing recovery codes will be invalidated.

Validation

After logging in, user's TOTP state must be checked with enabled?/1.

If enabled, user must be presented with TOTP code input form accepting 6 digit characters. The code must be checked using validate_code/2.

User must have an option to alternatively input one of their recovery codes. Those codes must be checked with use_recovery_code/2.

Code validity

In case of TOTP codes, a grace period of 30 seconds is applied, which allows user to use their current and previous TOTP code, assuming 30 second validity window of each. This allows user to use code that was about to expire before the submission. Regardless of that, each TOTP code can be used only once. Validation procedure rejects repeat use of the same code for safety. It's done by tracking last time a TOTP code was used successfully, stored under User.totp_last_used_at.

In case of recovery codes, each code is deleted immediately after use. They are strictly one-time use only.

TOTP Token

TOTP token is an alternate method of authenticating user session. It's main use case is "trust this device" functionality, where user can decide to skip 2FA verification for a particular browser session for next N days. The token should then be stored in an encrypted, signed cookie with a proper expiration timestamp.

The token should be reset each time it either fails to match or when other credentials (like password) are reset. This should effectively invalidate all trusted devices for a given user.

Summary

Functions

@spec disable(Plausible.Auth.User.t(), String.t()) ::
  {:ok, Plausible.Auth.User.t()} | {:error, :invalid_password}
Link to this function

enable(user, code, opts \\ [])

View Source
@spec enable(Plausible.Auth.User.t(), String.t() | :skip_verify, Keyword.t()) ::
  {:ok, Plausible.Auth.User.t(), %{recovery_codes: [String.t()]}}
  | {:error, :invalid_code | :not_initiated}
@spec enabled?(Plausible.Auth.User.t()) :: boolean()
Link to this function

generate_recovery_codes(map)

View Source
Link to this function

generate_recovery_codes(user, password)

View Source
@spec generate_recovery_codes(Plausible.Auth.User.t(), String.t()) ::
  {:ok, [String.t()]} | {:error, :invalid_password | :not_enabled}
@spec initiate(Plausible.Auth.User.t()) ::
  {:ok, Plausible.Auth.User.t(), %{totp_uri: String.t(), secret: String.t()}}
  | {:error, :not_verified | :already_setup}
@spec initiated?(Plausible.Auth.User.t()) :: boolean()
@spec reset_token(Plausible.Auth.User.t()) :: Plausible.Auth.User.t()
Link to this function

use_recovery_code(user, code)

View Source
@spec use_recovery_code(Plausible.Auth.User.t(), String.t()) ::
  :ok | {:error, :invalid_code | :not_enabled}
Link to this function

validate_code(user, code, opts \\ [])

View Source
@spec validate_code(Plausible.Auth.User.t(), String.t(), Keyword.t()) ::
  {:ok, Plausible.Auth.User.t()} | {:error, :invalid_code | :not_enabled}