mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-20 05:13:10 +12:00
improvement(AuthenticationFailed): store a caused_by
value in authentication failures. (#145)
This allows for a better debugging experience when trying to understand why an authentication action is failing. Closes #128.
This commit is contained in:
parent
b0dd8d55cd
commit
62cf54d85e
8 changed files with 212 additions and 13 deletions
|
@ -54,3 +54,5 @@ config :ash_authentication,
|
||||||
signing_secret: "Marty McFly in the past with the Delorean"
|
signing_secret: "Marty McFly in the past with the Delorean"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# config :ash_authentication, debug_authentication_failures?: true
|
||||||
|
|
|
@ -7,6 +7,8 @@ defmodule AshAuthentication.Application do
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
|
AshAuthentication.Debug.start()
|
||||||
|
|
||||||
[]
|
[]
|
||||||
|> maybe_append(
|
|> maybe_append(
|
||||||
start_dev_server?(),
|
start_dev_server?(),
|
||||||
|
|
65
lib/ash_authentication/debug.ex
Normal file
65
lib/ash_authentication/debug.ex
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
defmodule AshAuthentication.Debug do
|
||||||
|
@moduledoc """
|
||||||
|
Allows you to debug authentication failures in development.
|
||||||
|
|
||||||
|
Simply add `config :ash_authentication, debug_authentication_failures?: true`
|
||||||
|
to your `dev.exs` and get fancy log messages when authentication fails.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias AshAuthentication.Errors.AuthenticationFailed
|
||||||
|
require Logger
|
||||||
|
import AshAuthentication.Utils
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec start :: :ok
|
||||||
|
def start do
|
||||||
|
if enabled?() do
|
||||||
|
Logger.warning("""
|
||||||
|
Starting AshAuthentication with `debug_authentication_failres?` turned on.
|
||||||
|
|
||||||
|
You should only ever do this in your development environment for
|
||||||
|
debugging purposes as it will leak PII into your log.
|
||||||
|
|
||||||
|
If you do not want this on then please remove the following line from
|
||||||
|
your configuration:
|
||||||
|
|
||||||
|
config :ash_authentication, debug_authentication_failures?: true
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec describe(value) :: value when value: any
|
||||||
|
def describe(auth_failed) when is_struct(auth_failed, AuthenticationFailed) do
|
||||||
|
if enabled?() do
|
||||||
|
message =
|
||||||
|
case auth_failed.caused_by do
|
||||||
|
exception when is_exception(exception) -> Exception.message(exception)
|
||||||
|
%{message: message} -> message
|
||||||
|
_ -> "Unknown reason"
|
||||||
|
end
|
||||||
|
|
||||||
|
Logger.warning("""
|
||||||
|
Authentication failed: #{message}
|
||||||
|
|
||||||
|
Details: #{inspect(auth_failed, limit: :infinity, printable_limit: :infinity, pretty: true)}
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
|
||||||
|
auth_failed
|
||||||
|
end
|
||||||
|
|
||||||
|
def describe(other), do: other
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Has authentication debug logging been enabled?
|
||||||
|
"""
|
||||||
|
@spec enabled? :: boolean
|
||||||
|
def enabled? do
|
||||||
|
:ash_authentication
|
||||||
|
|> Application.get_env(:debug_authentication_failures?, false)
|
||||||
|
|> is_truthy()
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,10 +3,17 @@ defmodule AshAuthentication.Errors.AuthenticationFailed do
|
||||||
A generic, authentication failed error.
|
A generic, authentication failed error.
|
||||||
"""
|
"""
|
||||||
use Ash.Error.Exception
|
use Ash.Error.Exception
|
||||||
def_ash_error([], class: :forbidden)
|
def_ash_error([caused_by: %{}], class: :forbidden)
|
||||||
|
import AshAuthentication.Debug
|
||||||
|
|
||||||
@type t :: Exception.t()
|
@type t :: Exception.t()
|
||||||
|
|
||||||
|
def exception(args) do
|
||||||
|
args
|
||||||
|
|> super()
|
||||||
|
|> describe()
|
||||||
|
end
|
||||||
|
|
||||||
defimpl Ash.ErrorKind do
|
defimpl Ash.ErrorKind do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
def id(_), do: Ecto.UUID.generate()
|
def id(_), do: Ecto.UUID.generate()
|
||||||
|
|
|
@ -34,8 +34,47 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
|
||||||
|> Query.for_read(strategy.sign_in_action_name, params)
|
|> Query.for_read(strategy.sign_in_action_name, params)
|
||||||
|> api.read(options)
|
|> api.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, [user]} -> {:ok, user}
|
{:ok, [user]} ->
|
||||||
_ -> {:error, Errors.AuthenticationFailed.exception([])}
|
{:ok, user}
|
||||||
|
|
||||||
|
{:ok, []} ->
|
||||||
|
{:error,
|
||||||
|
Errors.AuthenticationFailed.exception(
|
||||||
|
caused_by: %{
|
||||||
|
module: __MODULE__,
|
||||||
|
strategy: strategy,
|
||||||
|
action: :sign_in,
|
||||||
|
message: "Query returned no users"
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
|
||||||
|
{:ok, _users} ->
|
||||||
|
{:error,
|
||||||
|
Errors.AuthenticationFailed.exception(
|
||||||
|
caused_by: %{
|
||||||
|
module: __MODULE__,
|
||||||
|
strategy: strategy,
|
||||||
|
action: :sign_in,
|
||||||
|
message: "Query returned too many users"
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
|
||||||
|
{:error, error} when is_struct(error, Errors.AuthenticationFailed) ->
|
||||||
|
{:error, error}
|
||||||
|
|
||||||
|
{:error, error} when is_exception(error) ->
|
||||||
|
{:error, Errors.AuthenticationFailed.exception(caused_by: error)}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error,
|
||||||
|
Errors.AuthenticationFailed.exception(
|
||||||
|
caused_by: %{
|
||||||
|
module: __MODULE__,
|
||||||
|
strategy: strategy,
|
||||||
|
action: :sign_in,
|
||||||
|
message: "Query returned error: #{inspect(error)}"
|
||||||
|
}
|
||||||
|
)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
|
||||||
3. Updates the user identity resource, if one is enabled.
|
3. Updates the user identity resource, if one is enabled.
|
||||||
"""
|
"""
|
||||||
use Ash.Resource.Preparation
|
use Ash.Resource.Preparation
|
||||||
alias Ash.{Error.Framework.AssumptionFailed, Query, Resource.Preparation}
|
alias Ash.{Query, Resource.Preparation}
|
||||||
alias AshAuthentication.{Errors.AuthenticationFailed, Info, Jwt, UserIdentity}
|
alias AshAuthentication.{Errors.AuthenticationFailed, Info, Jwt, UserIdentity}
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
import AshAuthentication.Utils, only: [is_falsy: 1]
|
import AshAuthentication.Utils, only: [is_falsy: 1]
|
||||||
|
@ -22,7 +22,14 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
|
||||||
case Info.strategy_for_action(query.resource, query.action.name) do
|
case Info.strategy_for_action(query.resource, query.action.name) do
|
||||||
:error ->
|
:error ->
|
||||||
{:error,
|
{:error,
|
||||||
AssumptionFailed.exception(message: "Strategy is missing from the changeset context.")}
|
AuthenticationFailed.exception(
|
||||||
|
query: query,
|
||||||
|
caused_by: %{
|
||||||
|
module: __MODULE__,
|
||||||
|
action: query.action,
|
||||||
|
message: "Unable to infer strategy"
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
|
||||||
{:ok, strategy} ->
|
{:ok, strategy} ->
|
||||||
query
|
query
|
||||||
|
@ -33,7 +40,16 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
|
||||||
end
|
end
|
||||||
|
|
||||||
_, _ ->
|
_, _ ->
|
||||||
{:error, AuthenticationFailed.exception(query: query)}
|
{:error,
|
||||||
|
AuthenticationFailed.exception(
|
||||||
|
query: query,
|
||||||
|
caused_by: %{
|
||||||
|
module: __MODULE__,
|
||||||
|
action: query.action,
|
||||||
|
strategy: strategy,
|
||||||
|
message: "Query should return a single user"
|
||||||
|
}
|
||||||
|
)}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,8 +27,44 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
||||||
|> Query.for_read(strategy.sign_in_action_name, params)
|
|> Query.for_read(strategy.sign_in_action_name, params)
|
||||||
|> api.read(options)
|
|> api.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, [user]} -> {:ok, user}
|
{:ok, [user]} ->
|
||||||
_ -> {:error, Errors.AuthenticationFailed.exception([])}
|
{:ok, user}
|
||||||
|
|
||||||
|
{:ok, []} ->
|
||||||
|
{:error,
|
||||||
|
Errors.AuthenticationFailed.exception(
|
||||||
|
caused_by: %{
|
||||||
|
module: __MODULE__,
|
||||||
|
strategy: strategy,
|
||||||
|
action: :sign_in,
|
||||||
|
message: "Query returned no users"
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
|
||||||
|
{:ok, _users} ->
|
||||||
|
{:error,
|
||||||
|
Errors.AuthenticationFailed.exception(
|
||||||
|
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(caused_by: error)}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error,
|
||||||
|
Errors.AuthenticationFailed.exception(
|
||||||
|
caused_by: %{
|
||||||
|
module: __MODULE__,
|
||||||
|
strategy: strategy,
|
||||||
|
action: :sign_in,
|
||||||
|
message: "Query returned error: #{inspect(error)}"
|
||||||
|
}
|
||||||
|
)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -36,16 +36,48 @@ defmodule AshAuthentication.Strategy.Password.SignInPreparation do
|
||||||
Map.get(record, strategy.hashed_password_field)
|
Map.get(record, strategy.hashed_password_field)
|
||||||
),
|
),
|
||||||
do: {:ok, [maybe_generate_token(record)]},
|
do: {:ok, [maybe_generate_token(record)]},
|
||||||
else: auth_failed(query)
|
else:
|
||||||
|
{:error,
|
||||||
|
AuthenticationFailed.exception(
|
||||||
|
query: query,
|
||||||
|
caused_by: %{
|
||||||
|
module: __MODULE__,
|
||||||
|
action: query.action,
|
||||||
|
resource: query.resource,
|
||||||
|
message: "Password is not valid"
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
|
||||||
_, _ ->
|
query, [] ->
|
||||||
strategy.hash_provider.simulate()
|
strategy.hash_provider.simulate()
|
||||||
auth_failed(query)
|
|
||||||
|
{:error,
|
||||||
|
AuthenticationFailed.exception(
|
||||||
|
query: query,
|
||||||
|
caused_by: %{
|
||||||
|
module: __MODULE__,
|
||||||
|
strategy: strategy,
|
||||||
|
action: :sign_in,
|
||||||
|
message: "Query returned no users"
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
|
||||||
|
query, users when is_list(users) ->
|
||||||
|
strategy.hash_provider.simulate()
|
||||||
|
|
||||||
|
{:error,
|
||||||
|
AuthenticationFailed.exception(
|
||||||
|
query: query,
|
||||||
|
caused_by: %{
|
||||||
|
module: __MODULE__,
|
||||||
|
strategy: strategy,
|
||||||
|
action: :sign_in,
|
||||||
|
message: "Query returned too many users"
|
||||||
|
}
|
||||||
|
)}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp auth_failed(query), do: {:error, AuthenticationFailed.exception(query: query)}
|
|
||||||
|
|
||||||
defp maybe_generate_token(record) do
|
defp maybe_generate_token(record) do
|
||||||
if AshAuthentication.Info.authentication_tokens_enabled?(record.__struct__) do
|
if AshAuthentication.Info.authentication_tokens_enabled?(record.__struct__) do
|
||||||
{:ok, token, _claims} = Jwt.token_for_user(record)
|
{:ok, token, _claims} = Jwt.token_for_user(record)
|
||||||
|
|
Loading…
Reference in a new issue