Current section
Files
Jump to
Current section
Files
lib/strategy/firebase/strategy.ex
defimpl AshAuthentication.Strategy, for: AshAuthentication.Strategy.Firebase do
@moduledoc """
Authentication protocol implementation for firebase strategies.
"""
alias Ash.{Changeset, Query, Resource}
alias AshAuthentication.Errors
alias AshAuthentication.Errors.AuthenticationFailed
alias AshAuthentication.Firebase.Errors.{EmailNotVerified, InvalidToken}
alias AshAuthentication.Firebase.TokenVerifier
import AshAuthentication.Plug.Helpers, only: [store_authentication_result: 2]
require Ash.Query
require Logger
def name(strategy), do: strategy.name
def tokens_required?(_), do: false
def phases(_), do: [:sign_in]
def actions(_), do: [:sign_in]
def routes(strategy) do
subject_name = AshAuthentication.Info.authentication_subject_name!(strategy.resource)
[
{"/#{subject_name}/#{strategy.name}", :sign_in}
]
end
def method_for_phase(_, :sign_in), do: :post
def plug(strategy, :sign_in, conn) do
params = Map.take(conn.params, [to_string(strategy.token_input)])
result = action(strategy, :sign_in, params, context: %{conn: conn})
store_authentication_result(conn, result)
end
def action(strategy, :sign_in, params, options) do
context = Keyword.get(options, :context, %{})
with {:ok, project_id} <- fetch_secret(strategy, :project_id, context),
{:ok, firebase_token} <-
get_firebase_token_from_params(params, strategy.token_input),
{:ok, uid, fields} <- verify_firebase_token(firebase_token, project_id),
:ok <- check_email_verified(strategy, fields) do
user_info = build_user_info(fields, uid)
if strategy.registration_enabled? do
do_register(strategy, user_info, options)
else
do_sign_in(strategy, user_info, options)
end
else
{:error, :email_not_verified} ->
{:error, EmailNotVerified.exception(strategy: strategy.name)}
{:error, %InvalidToken{reason: reason}} ->
Logger.debug("Firebase token rejected",
reason: reason,
strategy: strategy.name
)
:telemetry.execute(
[:ash_authentication_firebase, :strategy, :token_rejected],
%{count: 1},
%{reason: reason, strategy: strategy.name}
)
{:error, Errors.InvalidToken.exception(type: :sign_in)}
{:error, %Errors.MissingSecret{} = error} ->
Logger.error("Firebase strategy misconfigured: missing secret",
path: error.path,
strategy: strategy.name
)
:telemetry.execute(
[:ash_authentication_firebase, :strategy, :missing_secret],
%{count: 1},
%{strategy: strategy.name, path: error.path}
)
{:error, Errors.InvalidToken.exception(type: :sign_in)}
_ ->
{:error, Errors.InvalidToken.exception(type: :sign_in)}
end
end
defp do_register(strategy, user_info, options) do
action = Resource.Info.action(strategy.resource, strategy.register_action_name, :create)
strategy.resource
|> Changeset.new()
|> Changeset.set_context(%{
private: %{
ash_authentication?: true
}
})
|> Changeset.for_create(
strategy.register_action_name,
%{user_info: user_info},
upsert?: true,
upsert_identity: action.upsert_identity
)
|> Ash.create(options)
end
defp do_sign_in(strategy, user_info, options) do
strategy.resource
|> Query.new()
|> Query.set_context(%{
private: %{
ash_authentication?: true
}
})
|> Query.for_read(strategy.sign_in_action_name, %{user_info: user_info})
|> Ash.read(options)
|> case do
{:ok, [user]} ->
{:ok, user}
{:ok, []} ->
{:error,
AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :sign_in,
message: "Query returned no users"
}
)}
{:ok, _users} ->
{:error,
AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :sign_in,
message: "Query returned too many users"
}
)}
{:error, %AuthenticationFailed{} = error} ->
{:error, error}
{:error, error} when is_exception(error) ->
{:error,
AuthenticationFailed.exception(
strategy: strategy,
caused_by: error
)}
{:error, other} ->
{:error,
AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
action: :sign_in,
reason: other
}
)}
end
end
defp check_email_verified(strategy, fields) do
if strategy.require_email_verified? and email_unverified?(fields) do
{:error, :email_not_verified}
else
:ok
end
end
defp email_unverified?(fields) do
is_binary(fields["email"]) and fields["email"] != "" and
fields["email_verified"] != true
end
defp verify_firebase_token(token, project_id) do
TokenVerifier.verify(token, project_id)
end
defp get_firebase_token_from_params(params, token_input) do
case Map.get(params, token_input) || Map.get(params, Atom.to_string(token_input)) do
token when is_binary(token) and token != "" -> {:ok, token}
_ -> {:error, :missing_token}
end
end
defp fetch_secret(strategy, secret_name, context) do
path = [:authentication, :strategies, strategy.name, secret_name]
with {:ok, {secret_module, secret_opts}} <- Map.fetch(strategy, secret_name),
{:ok, secret} when is_binary(secret) and byte_size(secret) > 0 <-
AshAuthentication.Secret.secret_for(
secret_module,
path,
strategy.resource,
secret_opts,
context
) do
{:ok, secret}
else
{:ok, secret} when is_binary(secret) and byte_size(secret) > 0 ->
{:ok, secret}
_ ->
{:error, Errors.MissingSecret.exception(path: path, resource: strategy.resource)}
end
end
defp build_user_info(fields, uid) do
fields
|> Map.put("uid", uid)
end
end