Packages
ash_authentication
5.0.0-rc.6
5.0.0-rc.12
5.0.0-rc.11
5.0.0-rc.10
5.0.0-rc.9
5.0.0-rc.8
5.0.0-rc.7
5.0.0-rc.6
5.0.0-rc.5
5.0.0-rc.4
5.0.0-rc.3
5.0.0-rc.2
5.0.0-rc.1
5.0.0-rc.0
4.14.1
4.14.0
4.13.7
4.13.6
4.13.5
4.13.4
4.13.3
4.13.2
retired
4.13.1
retired
4.13.0
4.12.0
4.11.0
4.10.0
4.9.9
4.9.8
4.9.7
4.9.6
4.9.5
4.9.4
4.9.3
4.9.2
4.9.1
4.9.0
4.8.7
4.8.6
4.8.5
4.8.3
4.8.2
4.8.1
4.8.0
4.7.6
4.7.5
4.7.4
4.7.3
4.7.2
4.7.1
4.7.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.6
4.5.5
4.5.4
4.5.3
4.5.2
4.5.1
4.5.0
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.12
4.3.11
4.3.10
4.3.9
4.3.8
4.3.7
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.4
4.0.3
4.0.2
4.0.1
4.0.0
4.0.0-rc.6
4.0.0-rc.5
4.0.0-rc.3
4.0.0-rc.2
4.0.0-rc.1
4.0.0-rc.0
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.16
3.11.15
3.11.14
3.11.13
3.11.12
3.11.11
3.11.10
3.11.9
3.11.8
3.11.7
3.11.6
3.11.5
3.11.4
3.11.3
3.11.2
3.11.1
3.11.0
3.10.8
3.10.7
3.10.6
3.10.5
3.10.4
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.0
3.7.9
3.7.8
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
3.6.1
3.6.0
3.5.3
3.5.2
3.5.1
3.5.0
3.3.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.0
3.0.3
Authentication extension for the Ash Framework.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash_authentication/strategies/totp/confirm_setup_change.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.Totp.ConfirmSetupChange do
@moduledoc """
Confirms a pending TOTP setup by verifying a code and storing the secret.
This change is used when `confirm_setup_enabled?` is true. It:
1. Validates the TOTP code format (6 digits)
2. Verifies the setup_token JWT
3. Retrieves the pending secret from the token resource
4. Verifies the TOTP code against the secret
5. Stores the secret on the user
6. Revokes the setup token (after successful storage)
Token revocation is performed after the secret is stored to avoid losing the
token if storage fails for any reason.
This ensures the user has correctly saved their TOTP secret before it's activated.
"""
use Ash.Resource.Change
alias Ash.Changeset
alias Ash.Error.Framework.AssumptionFailed
alias AshAuthentication.{Errors.AuthenticationFailed, Info, Jwt, TokenResource}
alias AshAuthentication.Strategy.Totp.Helpers
@doc false
@impl true
def change(changeset, _context, _opts) do
case Info.strategy_for_action(changeset.resource, changeset.action.name) do
{:ok, strategy} ->
do_change(changeset, strategy)
:error ->
raise AssumptionFailed,
message: "Action does not correlate with an authentication strategy"
end
end
defp do_change(changeset, strategy) do
changeset
|> Changeset.set_context(%{private: %{ash_authentication?: true}})
|> Changeset.before_action(fn changeset ->
setup_token = Changeset.get_argument(changeset, :setup_token)
code = Changeset.get_argument(changeset, :code)
with :ok <- validate_code_format(code, strategy),
{:ok, secret} <- verify_token_and_get_secret(setup_token, strategy),
:ok <- verify_code(secret, code, strategy) do
changeset
|> Changeset.force_change_attribute(strategy.secret_field, secret)
|> Changeset.put_context(:setup_token_to_revoke, setup_token)
else
{:error, reason} ->
Changeset.add_error(changeset, reason)
end
end)
|> Changeset.after_action(&maybe_revoke_setup_token(&1, &2, strategy))
|> Changeset.after_action(&preserve_authentication_metadata/2)
end
defp maybe_revoke_setup_token(changeset, result, strategy) do
case changeset.context[:setup_token_to_revoke] do
nil -> {:ok, result}
setup_token -> revoke_token_for_result(setup_token, result, strategy)
end
end
defp revoke_token_for_result(setup_token, result, strategy) do
case revoke_token(setup_token, strategy) do
:ok -> {:ok, result}
{:error, reason} -> {:error, reason}
end
end
defp validate_code_format(code, strategy) do
case Helpers.validate_totp_code(code) do
:ok -> :ok
{:error, :invalid_format} -> {:error, invalid_code_format_error(strategy)}
end
end
defp verify_token_and_get_secret(setup_token, strategy) do
with {:ok, %{"jti" => jti}, _resource} <- Jwt.verify(setup_token, strategy.resource),
{:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
{:ok, [token_record]} <-
TokenResource.Actions.get_token(
token_resource,
%{"jti" => jti, "purpose" => "totp_setup"},
[]
),
{:ok, encoded_secret} <- get_extra_data_secret(token_record) do
case Base.decode64(encoded_secret) do
{:ok, secret} -> {:ok, secret}
:error -> {:error, invalid_token_error(strategy, "Invalid secret encoding")}
end
else
{:ok, []} ->
{:error, invalid_token_error(strategy, "Setup token not found or expired")}
{:ok, _tokens} ->
{:error, invalid_token_error(strategy, "Ambiguous setup token")}
:error ->
{:error, invalid_token_error(strategy, "Invalid setup token")}
{:error, reason} when is_exception(reason) ->
{:error, reason}
{:error, reason} ->
{:error, invalid_token_error(strategy, "Token verification failed: #{inspect(reason)}")}
end
end
defp get_extra_data_secret(token_record) do
case token_record.extra_data do
%{"secret" => secret} when is_binary(secret) -> {:ok, secret}
_ -> {:error, :missing_secret}
end
end
defp verify_code(secret, code, strategy) do
if Helpers.valid_totp?(secret, code, strategy) do
:ok
else
{:error, invalid_code_error(strategy)}
end
end
defp revoke_token(setup_token, strategy) do
with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource) do
# TOTP setup tokens are always stored (with the pending secret in
# `extra_data`) regardless of the `store_all_tokens?` flag, so we always
# take the lock-and-update revocation path.
TokenResource.Actions.revoke(token_resource, setup_token, store_all_tokens?: true)
end
end
defp invalid_token_error(strategy, message) do
AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :confirm_setup,
message: message
}
)
end
defp invalid_code_error(strategy) do
AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :confirm_setup,
message: "Invalid TOTP code"
}
)
end
defp invalid_code_format_error(strategy) do
AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :confirm_setup,
message: "Invalid TOTP code format"
}
)
end
# Keys that should be preserved from the input user to the result
@preserved_metadata_keys [:token, :authentication_strategies, :totp_verified_at]
defp preserve_authentication_metadata(changeset, result) do
original_metadata = changeset.data.__metadata__ || %{}
result =
@preserved_metadata_keys
|> Enum.reduce(result, fn key, acc ->
case Map.get(original_metadata, key) do
nil -> acc
value -> Ash.Resource.put_metadata(acc, key, value)
end
end)
{:ok, result}
end
end