Skip to content

Commit

Permalink
fix: ensure that tenant is set and ferried through all operations
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel committed Feb 18, 2025
1 parent 19f3675 commit dfc84fb
Show file tree
Hide file tree
Showing 17 changed files with 146 additions and 93 deletions.
15 changes: 12 additions & 3 deletions lib/ash_authentication/add_ons/confirmation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,22 @@ defmodule AshAuthentication.AddOn.Confirmation do
This will generate a token with the `"act"` claim set to the confirmation
action for the strategy, and the `"chg"` claim will contain any changes.
"""
@spec confirmation_token(Confirmation.t(), Changeset.t(), Resource.record()) ::
@spec confirmation_token(
Confirmation.t(),
Changeset.t(),
Resource.record(),
opts :: Keyword.t()
) ::
{:ok, String.t()} | :error | {:error, any}
def confirmation_token(strategy, changeset, user) do
def confirmation_token(strategy, changeset, user, opts \\ []) do
claims = %{"act" => strategy.confirm_action_name}

with {:ok, token, _claims} <-
Jwt.token_for_user(user, claims, token_lifetime: strategy.token_lifetime),
Jwt.token_for_user(
user,
claims,
Keyword.merge(opts, token_lifetime: strategy.token_lifetime)
),
:ok <- Confirmation.Actions.store_changes(strategy, token, changeset) do
{:ok, token}
end
Expand Down
2 changes: 1 addition & 1 deletion lib/ash_authentication/add_ons/confirmation/actions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
@spec confirm(Confirmation.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def confirm(strategy, params, opts \\ []) do
with {:ok, token} <- Map.fetch(params, "confirm"),
{:ok, %{"sub" => subject}, _} <- Jwt.verify(token, strategy.resource),
{:ok, %{"sub" => subject}, _} <- Jwt.verify(token, strategy.resource, opts),
{:ok, user} <- AshAuthentication.subject_to_user(subject, strategy.resource, opts),
{:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource) do
opts =
Expand Down
8 changes: 4 additions & 4 deletions lib/ash_authentication/add_ons/confirmation/confirm_change.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmChange do
@doc false
@impl true
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
def change(changeset, _opts, _context) do
def change(changeset, _opts, context) do
case Info.strategy_for_action(changeset.resource, changeset.action.name) do
{:ok, strategy} ->
do_change(changeset, strategy)
do_change(changeset, strategy, context)

:error ->
raise AssumptionFailed,
message: "Action does not correlate with an authentication strategy"
end
end

defp do_change(changeset, strategy) do
defp do_change(changeset, strategy, context) do
changeset
|> Changeset.set_context(%{
private: %{
Expand All @@ -38,7 +38,7 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmChange do
with token when is_binary(token) <-
Changeset.get_argument(changeset, :confirm),
{:ok, %{"act" => action, "jti" => jti}, _} <-
Jwt.verify(token, changeset.resource),
Jwt.verify(token, changeset.resource, Ash.Context.to_opts(context)),
true <-
to_string(strategy.confirm_action_name) == action,
{:ok, changes} <- Actions.get_changes(strategy, jti) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do
def change(changeset, options, context) do
case Info.find_strategy(changeset, context, options) do
{:ok, strategy} ->
do_change(changeset, strategy)
do_change(changeset, strategy, context)

:error ->
changeset
end
end

defp do_change(changeset, strategy) do
defp do_change(changeset, strategy, context) do
auto_confirm? = changeset.action.name in strategy.auto_confirm_actions

changeset =
Expand All @@ -72,7 +72,7 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do
if auto_confirm? do
changeset
else
Ash.Changeset.before_action(changeset, &before_action(&1, strategy))
Ash.Changeset.before_action(changeset, &before_action(&1, strategy, context))
end
end)
end
Expand All @@ -81,14 +81,14 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do
def atomic(changeset, opts, context) do
case Info.find_strategy(changeset, context, opts) do
{:ok, strategy} ->
atomic_confirmation(changeset, strategy)
atomic_confirmation(changeset, strategy, context)

:error ->
changeset
end
end

defp atomic_confirmation(changeset, strategy) do
defp atomic_confirmation(changeset, strategy, context) do
auto_confirm? = changeset.action.name in strategy.auto_confirm_actions

if auto_confirm? do
Expand Down Expand Up @@ -124,7 +124,7 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do

true ->
{:ok,
maybe_perform_confirmation(changeset, strategy, changeset)
maybe_perform_confirmation(changeset, strategy, changeset, context)
|> validate_identities(strategy, true)}
end
end
Expand All @@ -137,14 +137,14 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do
end)
end

defp before_action(changeset, strategy) do
defp before_action(changeset, strategy, context) do
changeset
|> not_confirm_action(strategy)
|> should_confirm_action_type(strategy)
|> monitored_field_changing(strategy)
|> changes_would_be_valid()
|> maybe_inhibit_updates(strategy)
|> maybe_perform_confirmation(strategy, changeset)
|> maybe_perform_confirmation(strategy, changeset, context)
end

defp handle_upserts(
Expand Down Expand Up @@ -321,12 +321,12 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do
|> Ash.exists?(tenant: changeset.tenant)
end

defp maybe_perform_confirmation(%Changeset{} = changeset, strategy, original_changeset) do
defp maybe_perform_confirmation(%Changeset{} = changeset, strategy, original_changeset, context) do
changeset
|> nil_confirmed_at_field(strategy)
|> Changeset.after_action(fn _changeset, user ->
strategy
|> Confirmation.confirmation_token(original_changeset, user)
|> Confirmation.confirmation_token(original_changeset, user, Ash.Context.to_opts(context))
|> case do
{:ok, token} ->
{sender, send_opts} = strategy.sender
Expand All @@ -348,7 +348,7 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do
end)
end

defp maybe_perform_confirmation(_changeset, _strategy, original_changeset),
defp maybe_perform_confirmation(_changeset, _strategy, original_changeset, _context),
do: original_changeset

defp nil_confirmed_at_field(changeset, strategy) do
Expand Down
17 changes: 11 additions & 6 deletions lib/ash_authentication/generate_token_change.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule AshAuthentication.GenerateTokenChange do
{:ok, strategy} = Info.find_strategy(changeset, context, options)

if Info.authentication_tokens_enabled?(result.__struct__) do
{:ok, generate_token(changeset.context[:token_type] || :user, result, strategy)}
{:ok, generate_token(changeset.context[:token_type] || :user, result, strategy, context)}
else
{:ok, result}
end
Expand All @@ -28,18 +28,23 @@ defmodule AshAuthentication.GenerateTokenChange do
{:ok, change(changeset, options, context)}
end

defp generate_token(purpose, record, strategy)
defp generate_token(purpose, record, strategy, context)
when is_integer(strategy.sign_in_token_lifetime) and purpose == :sign_in do
{:ok, token, _claims} =
Jwt.token_for_user(record, %{"purpose" => to_string(purpose)},
token_lifetime: strategy.sign_in_token_lifetime
Jwt.token_for_user(
record,
%{"purpose" => to_string(purpose)},
Ash.Context.to_opts(context,
token_lifetime: strategy.sign_in_token_lifetime
)
)

Ash.Resource.put_metadata(record, :token, token)
end

defp generate_token(purpose, record, _strategy) do
{:ok, token, _claims} = Jwt.token_for_user(record, %{"purpose" => to_string(purpose)})
defp generate_token(purpose, record, _strategy, context) do
{:ok, token, _claims} =
Jwt.token_for_user(record, %{"purpose" => to_string(purpose)}, Ash.Context.to_opts(context))

Ash.Resource.put_metadata(record, :token, token)
end
Expand Down
17 changes: 9 additions & 8 deletions lib/ash_authentication/jwt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -184,32 +184,33 @@ defmodule AshAuthentication.Jwt do
@doc """
Given a token, verify it's signature and validate it's claims.
"""
@spec verify(token, Resource.t() | atom) :: {:ok, claims, Resource.t()} | :error
def verify(token, otp_app_or_resource) do
@spec verify(token, Resource.t() | atom, opts :: Keyword.t()) ::
{:ok, claims, Resource.t()} | :error
def verify(token, otp_app_or_resource, opts \\ []) do
if function_exported?(otp_app_or_resource, :spark_is, 0) &&
otp_app_or_resource.spark_is() == Resource do
verify_for_resource(token, otp_app_or_resource)
verify_for_resource(token, otp_app_or_resource, opts)
else
verify_for_otp_app(token, otp_app_or_resource)
verify_for_otp_app(token, otp_app_or_resource, opts)
end
end

defp verify_for_resource(token, resource) do
defp verify_for_resource(token, resource, opts) do
with signer <- Config.token_signer(resource),
{:ok, claims} <- Joken.verify(token, signer),
defaults <- Config.default_claims(resource),
defaults <- Config.default_claims(resource, opts),
{:ok, claims} <- Joken.validate(defaults, claims, resource) do
{:ok, claims, resource}
else
_ -> :error
end
end

defp verify_for_otp_app(token, otp_app) do
defp verify_for_otp_app(token, otp_app, opts) do
with {:ok, resource} <- token_to_resource(token, otp_app),
signer <- Config.token_signer(resource),
{:ok, claims} <- Joken.verify(token, signer),
defaults <- Config.default_claims(resource),
defaults <- Config.default_claims(resource, opts),
{:ok, claims} <- Joken.validate(defaults, claims, resource) do
{:ok, claims, resource}
else
Expand Down
29 changes: 15 additions & 14 deletions lib/ash_authentication/plug/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ defmodule AshAuthentication.Plug.Helpers do
"""
@spec retrieve_from_session(Conn.t(), module, keyword) :: Conn.t()
def retrieve_from_session(conn, otp_app, opts \\ []) do
opts =
opts
|> Keyword.put_new(:tenant, Ash.PlugHelpers.get_tenant(conn))
|> Keyword.put_new(:context, Ash.PlugHelpers.get_context(conn) || %{})

otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(
Expand All @@ -79,26 +84,21 @@ defmodule AshAuthentication.Plug.Helpers do
with token when is_binary(token) <-
Conn.get_session(conn, "#{options.subject_name}_token"),
{:ok, %{"sub" => subject, "jti" => jti} = claims, _}
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app),
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app, opts),
{:ok, [_]} <-
TokenResource.Actions.get_token(
token_resource,
%{
"jti" => jti,
"purpose" => "user"
},
tenant: Ash.PlugHelpers.get_tenant(conn),
context: Ash.PlugHelpers.get_context(conn) || %{}
opts
),
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
[
tenant: Ash.PlugHelpers.get_tenant(conn),
context: Ash.PlugHelpers.get_context(conn) || %{}
]
|> Keyword.merge(opts)
opts
) do
Conn.assign(conn, current_subject_name, user)
else
Expand Down Expand Up @@ -137,24 +137,25 @@ defmodule AshAuthentication.Plug.Helpers do
"""
@spec retrieve_from_bearer(Conn.t(), module, keyword) :: Conn.t()
def retrieve_from_bearer(conn, otp_app, opts \\ []) do
opts =
opts
|> Keyword.put_new(:tenant, Ash.PlugHelpers.get_tenant(conn))
|> Keyword.put_new(:context, Ash.PlugHelpers.get_context(conn) || %{})

conn
|> Conn.get_req_header("authorization")
|> Stream.filter(&String.starts_with?(&1, "Bearer "))
|> Stream.map(&String.replace_leading(&1, "Bearer ", ""))
|> Enum.reduce(conn, fn token, conn ->
with {:ok, %{"sub" => subject, "jti" => jti} = claims, resource}
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app),
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app, opts),
{:ok, token_record} <-
validate_token(resource, jti),
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
[
tenant: Ash.PlugHelpers.get_tenant(conn),
context: Ash.PlugHelpers.get_context(conn) || %{}
]
|> Keyword.merge(opts)
opts
),
{:ok, subject_name} <- Info.authentication_subject_name(resource),
current_subject_name <- current_subject_name(subject_name) do
Expand Down
10 changes: 6 additions & 4 deletions lib/ash_authentication/strategies/magic_link.ex
Original file line number Diff line number Diff line change
Expand Up @@ -149,17 +149,19 @@ defmodule AshAuthentication.Strategy.MagicLink do
Used by `AshAuthentication.Strategy.MagicLink.RequestPreparation`.
"""
@spec request_token_for(t, Resource.record()) :: {:ok, binary} | :error
def request_token_for(strategy, user)
@spec request_token_for(t, Resource.record(), opts :: Keyword.t()) :: {:ok, binary} | :error
def request_token_for(strategy, user, opts \\ [])
when is_struct(strategy, __MODULE__) and is_struct(user, strategy.resource) do
case Jwt.token_for_user(
user,
%{
"act" => strategy.sign_in_action_name,
"identity" => Map.get(user, strategy.identity_field)
},
token_lifetime: strategy.token_lifetime,
purpose: :magic_link
Keyword.merge(opts,
token_lifetime: strategy.token_lifetime,
purpose: :magic_link
)
) do
{:ok, token, _claims} -> {:ok, token}
:error -> :error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule AshAuthentication.Strategy.MagicLink.SignInChange do
@doc false
@impl true
@spec change(Changeset.t(), keyword, Change.Context.t()) :: Changeset.t()
def change(changeset, _otps, _context) do
def change(changeset, _otps, context) do
subject_name =
changeset.resource
|> Info.authentication_subject_name!()
Expand All @@ -33,7 +33,7 @@ defmodule AshAuthentication.Strategy.MagicLink.SignInChange do
:ok = TokenResource.revoke(token_resource, token)
end

{:ok, token, _claims} = Jwt.token_for_user(record)
{:ok, token, _claims} = Jwt.token_for_user(record, %{}, Ash.Context.to_opts(context))
{:ok, Resource.put_metadata(record, :token, token)}

_changeset, {:error, error} ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule AshAuthentication.Strategy.MagicLink.SignInPreparation do
@doc false
@impl true
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, _otps, _context) do
def prepare(query, _otps, context) do
subject_name =
query.resource
|> Info.authentication_subject_name!()
Expand All @@ -21,7 +21,7 @@ defmodule AshAuthentication.Strategy.MagicLink.SignInPreparation do
with {:ok, strategy} <- Info.strategy_for_action(query.resource, query.action.name),
token when is_binary(token) <- Query.get_argument(query, strategy.token_param_name),
{:ok, %{"act" => token_action, "sub" => subject} = claims, _} <-
Jwt.verify(token, query.resource),
Jwt.verify(token, query.resource, Ash.Context.to_opts(context)),
^token_action <- to_string(strategy.sign_in_action_name),
%URI{path: ^subject_name, query: primary_key} <- URI.parse(subject) do
query
Expand Down Expand Up @@ -52,7 +52,7 @@ defmodule AshAuthentication.Strategy.MagicLink.SignInPreparation do
:ok = TokenResource.revoke(token_resource, token)
end

{:ok, token, _claims} = Jwt.token_for_user(record)
{:ok, token, _claims} = Jwt.token_for_user(record, %{}, Ash.Context.to_opts(context))
{:ok, [Resource.put_metadata(record, :token, token)]}

_query, [] ->
Expand Down
Loading

0 comments on commit dfc84fb

Please sign in to comment.