Packages
ash_authentication
5.0.0-rc.4
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/actions.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.Totp.Actions do
@moduledoc """
Actions for the TOTP strategy.
Provides the code interface for TOTP setup, sign-in, and verification.
"""
alias Ash.{ActionInput, Changeset, Query, Resource}
alias Ash.Error.Changes.Required
alias Ash.Error.Invalid
alias AshAuthentication.{Errors, Info, Strategy.Totp}
@doc """
Set up TOTP for a user by generating a new secret.
Takes a user record and runs the setup action which generates a new TOTP
secret. The user can then retrieve the `totp_url` calculation to display
a QR code for scanning with an authenticator app.
## Options
* `:domain` - The domain to use for the action. Defaults to the domain
configured on the user resource. This allows the strategy to work when
invoked from a different domain context (e.g., an admin domain managing
users from an accounts domain).
"""
@spec setup(Totp.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def setup(strategy, params, options) do
user = Map.get(params, :user) || Map.get(params, "user")
case user do
nil ->
{:error,
Invalid.exception(
errors: [
Required.exception(
resource: strategy.resource,
field: :user,
type: :argument
)
]
)}
user ->
options =
options
|> Keyword.put_new_lazy(:domain, fn -> Info.domain!(strategy.resource) end)
user
|> Changeset.new()
|> Changeset.set_context(%{private: %{ash_authentication?: true}})
|> Changeset.for_update(strategy.setup_action_name, %{}, options)
|> Ash.update()
|> case do
{:ok, user} ->
{:ok, user}
{:error, error} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: error
)}
end
end
end
@doc """
Confirm TOTP setup by verifying a code and activating the secret.
Used when `confirm_setup_enabled?` is true. Takes a user, setup_token, and
TOTP code. If the code is valid, the secret is stored on the user and the
setup token is revoked.
## Options
* `:domain` - The domain to use for the action. Defaults to the domain
configured on the user resource.
"""
@spec confirm_setup(Totp.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def confirm_setup(strategy, params, options) do
user = Map.get(params, :user) || Map.get(params, "user")
case user do
nil ->
{:error,
Invalid.exception(
errors: [
Required.exception(
resource: strategy.resource,
field: :user,
type: :argument
)
]
)}
user ->
options =
options
|> Keyword.put_new_lazy(:domain, fn -> Info.domain!(strategy.resource) end)
# Remove user from params since it's the changeset target, not an action argument
action_params =
params
|> Map.delete(:user)
|> Map.delete("user")
user
|> Changeset.new()
|> Changeset.set_context(%{private: %{ash_authentication?: true}})
|> Changeset.for_update(strategy.confirm_setup_action_name, action_params, options)
|> Ash.update()
|> case do
{:ok, user} ->
{:ok, user}
{:error, error} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: error
)}
end
end
end
@doc """
Sign in using a TOTP code.
Takes an identity (e.g., email) and a TOTP code, and returns the user if
the code is valid.
## Options
* `:domain` - The domain to use for the action. Defaults to the domain
configured on the user resource.
"""
@spec sign_in(Totp.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def sign_in(strategy, params, options) do
options =
options
|> Keyword.put_new_lazy(:domain, fn -> Info.domain!(strategy.resource) end)
strategy.resource
|> Query.new()
|> Query.set_context(%{private: %{ash_authentication?: true}})
|> Query.for_read(strategy.sign_in_action_name, params, options)
|> Ash.read()
|> case do
{:ok, [user]} ->
{:ok, user}
{:ok, []} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :sign_in,
message: "Query returned no users"
}
)}
{:ok, _users} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :sign_in,
message: "Query returned too many users"
}
)}
{:error, error} when is_exception(error) ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: error
)}
{:error, error} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :sign_in,
message: "Query returned error: #{inspect(error)}"
}
)}
end
end
@doc """
Verify a TOTP code for a user.
Takes a user and a TOTP code, and returns `{:ok, true}` if the code is valid
or `{:ok, false}` if it is not.
## Options
* `:domain` - The domain to use for the action. Defaults to the domain
configured on the user resource.
"""
@spec verify(Totp.t(), map, keyword) :: {:ok, boolean} | {:error, any}
def verify(strategy, params, options) do
options =
options
|> Keyword.put_new_lazy(:domain, fn -> Info.domain!(strategy.resource) end)
strategy.resource
|> ActionInput.new()
|> ActionInput.set_context(%{private: %{ash_authentication?: true}})
|> ActionInput.for_action(strategy.verify_action_name, params, options)
|> Ash.run_action()
|> case do
{:ok, result} ->
{:ok, result}
{:error, error} when is_exception(error) ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: error
)}
{:error, error} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :verify,
message: "Action returned error: #{inspect(error)}"
}
)}
end
end
end