Packages
ash_authentication
5.0.0-rc.11
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.
Current section
Files
Jump to
Current section
Files
lib/ash_authentication/strategies/otp/sign_in_preparation.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.Otp.SignInPreparation do
@moduledoc """
Prepare a query for OTP sign in.
This preparation:
1. Filters the query by the identity field.
2. After the query, computes the deterministic JTI from the submitted OTP code.
3. Looks up the stored OTP token by JTI with a SELECT FOR UPDATE lock to prevent
concurrent requests consuming the same code.
4. If found and valid, optionally revokes it (single-use), generates an auth JWT,
and returns the user with the token in metadata. Otherwise returns an
`AuthenticationFailed` error so the audit log records the attempt as a
failure.
"""
use Ash.Resource.Preparation
alias Ash.{Query, Resource, Resource.Preparation}
alias AshAuthentication.{
Errors.AuthenticationFailed,
Errors.InvalidToken,
Info,
Jwt,
Strategy.Otp
}
alias AshAuthentication.Strategy.Otp.SignInHelpers
require Ash.Query
@doc false
@impl true
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, _opts, context) do
strategy = Info.strategy_for_action!(query.resource, query.action.name)
identity_field = strategy.identity_field
identity = Query.get_argument(query, identity_field)
otp_code = Query.get_argument(query, strategy.otp_param_name)
if is_nil(identity) || is_nil(otp_code) do
Query.do_filter(query, false)
else
query
|> Query.filter(^ref(identity_field) == ^identity)
|> Query.after_action(&after_action(&1, &2, strategy, otp_code, context))
end
end
defp after_action(_query, [user], strategy, otp_code, context) do
context_opts = Ash.Context.to_opts(context)
subject = AshAuthentication.user_to_subject(user)
normalized_otp = Otp.normalize_otp(strategy, otp_code)
jti = Otp.compute_deterministic_jti(strategy, subject, normalized_otp)
token_resource = Info.authentication_tokens_token_resource!(strategy.resource)
# Read actions are not transactional by default, so wrap the lock + revocation
# in an explicit transaction to prevent TOCTOU races.
Ash.transaction(token_resource, fn ->
verify_and_sign_in(strategy, token_resource, jti, subject, user, context_opts)
end)
|> case do
{:ok, {:ok, _} = result} -> result
{:ok, {:error, _} = error} -> error
{:error, _} -> {:error, invalid_otp_error(strategy)}
end
end
defp after_action(_query, _users, strategy, _otp_code, _context) do
{:error, invalid_otp_error(strategy)}
end
defp verify_and_sign_in(strategy, token_resource, jti, subject, user, context_opts) do
with {:ok, [_ | _]} <- SignInHelpers.get_otp_token_locked(token_resource, jti, context_opts),
:ok <- SignInHelpers.consume_token(strategy, token_resource, jti, subject, context_opts) do
{:ok, auth_token, _claims} = Jwt.token_for_user(user, %{}, context_opts)
{:ok, [Resource.put_metadata(user, :token, auth_token)]}
else
_ -> {:error, invalid_otp_error(strategy)}
end
end
defp invalid_otp_error(strategy) do
AuthenticationFailed.exception(
strategy: strategy,
caused_by:
InvalidToken.exception(
field: strategy.otp_param_name,
reason: "Invalid or expired OTP code",
type: :otp
)
)
end
end