Packages
ash_authentication
4.14.1
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/api_key/sign_in_preparation.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.ApiKey.SignInPreparation do
@moduledoc """
Prepare a query for sign in.
"""
use Ash.Resource.Preparation
alias Ash.{Query, Resource.Preparation}
alias AshAuthentication.{Errors.AuthenticationFailed, Info}
require Ash.Query
@doc false
@impl true
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, opts, context) do
with {:ok, strategy} <- Info.find_strategy(query, context, opts),
{:ok, api_key} <- Query.fetch_argument(query, :api_key),
{:ok, api_key_id, random_bytes} <- decode_api_key(api_key) do
api_key_relationship =
Ash.Resource.Info.relationship(query.resource, strategy.api_key_relationship)
query
|> Query.set_context(%{private: %{ash_authentication?: true}})
|> Ash.Query.before_action(fn query ->
api_key_relationship.destination
|> Ash.Query.do_filter(api_key_relationship.filter)
|> Ash.Query.filter(id == ^api_key_id)
|> maybe_load_tenant(strategy.multitenancy_relationship)
|> Query.set_context(%{private: %{ash_authentication?: true}})
|> Ash.read_one()
|> case do
{:ok, nil} ->
Plug.Crypto.secure_compare(
:crypto.hash(:sha256, random_bytes <> api_key_id),
Ecto.UUID.bingenerate() <> :crypto.strong_rand_bytes(32)
)
Ash.Query.filter(query, false)
{:ok, api_key} ->
check_api_key(
query,
api_key,
api_key_id,
strategy,
api_key_relationship,
random_bytes
)
{:error, error} ->
Ash.Query.add_error(
query,
AuthenticationFailed.exception(
strategy: strategy,
query: query,
caused_by: error
)
)
end
end)
else
_ ->
Plug.Crypto.secure_compare(
:crypto.hash(:sha256, :crypto.strong_rand_bytes(32) <> Ecto.UUID.bingenerate()),
Ecto.UUID.bingenerate() <> :crypto.strong_rand_bytes(32)
)
Query.do_filter(query, false)
end
end
defp check_api_key(query, api_key, api_key_id, strategy, api_key_relationship, random_bytes) do
if Plug.Crypto.secure_compare(
:crypto.hash(:sha256, random_bytes <> api_key_id),
Map.get(api_key, strategy.api_key_hash_attribute)
) do
query
|> Ash.Query.do_filter(%{
api_key_relationship.source_attribute =>
Map.get(api_key, api_key_relationship.destination_attribute)
})
|> maybe_set_tenant(api_key, strategy.multitenancy_relationship)
|> Ash.Query.after_action(fn
_query, [user] ->
{:ok,
[
Ash.Resource.set_metadata(
user,
%{
api_key: api_key,
using_api_key?: true
}
)
]}
query, [] ->
{:error,
AuthenticationFailed.exception(
strategy: strategy,
query: query,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :sign_in,
message: "Query returned no users"
}
)}
query, _ ->
{:error,
AuthenticationFailed.exception(
strategy: strategy,
query: query,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :sign_in,
message: "Query returned too many users"
}
)}
end)
else
Ash.Query.filter(query, false)
end
end
defp decode_api_key(api_key) do
with [_parse_prefix, middle, crc32] <- String.split(api_key, "_", parts: 3),
{:ok, <<random_bytes::binary-size(32), id::binary-size(16)>>} <-
AshAuthentication.Base.bindecode62(middle),
true <-
AshAuthentication.Base.decode62(crc32) ==
{:ok, :erlang.crc32(random_bytes <> id)} do
{:ok, id, random_bytes}
else
_ ->
:error
end
end
defp maybe_load_tenant(api_key_query, nil), do: api_key_query
defp maybe_load_tenant(api_key_query, multitenancy_relationship) do
Ash.Query.load(api_key_query, multitenancy_relationship)
end
defp maybe_set_tenant(user_query, _api_key, nil), do: user_query
defp maybe_set_tenant(user_query, api_key, multitenancy_relationship) do
case Map.get(api_key, multitenancy_relationship) do
%{__struct__: _} = tenant -> Ash.Query.set_tenant(user_query, tenant)
_ -> user_query
end
end
end