Skip to content

Commit

Permalink
improvement: add sign in tokens to password strategy (#252)
Browse files Browse the repository at this point in the history
* improvement: add sign in tokens to password strategy

* chore: update `.formatter.exs`.

* chore: fix credo warnings.

* improvement: convert `sign_in_with_token` into an action.

---------

Co-authored-by: James Harton <[email protected]>
  • Loading branch information
zachdaniel and jimsynz authored Apr 6, 2023
1 parent 58adb09 commit eca8cad
Show file tree
Hide file tree
Showing 24 changed files with 603 additions and 91 deletions.
2 changes: 2 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ spark_locals_without_parens = [
sender: 1,
sign_in_action_name: 1,
sign_in_enabled?: 1,
sign_in_token_lifetime: 1,
sign_in_tokens_enabled?: 1,
signing_algorithm: 1,
signing_secret: 1,
single_use_token?: 1,
Expand Down
2 changes: 1 addition & 1 deletion lib/ash_authentication/errors/authentication_failed.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule AshAuthentication.Errors.AuthenticationFailed do
A generic, authentication failed error.
"""
use Ash.Error.Exception
def_ash_error([caused_by: %{}], class: :forbidden)
def_ash_error([:strategy, caused_by: %{}], class: :forbidden)
import AshAuthentication.Debug

@type t :: Exception.t()
Expand Down
24 changes: 14 additions & 10 deletions lib/ash_authentication/jwt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,30 +91,34 @@ defmodule AshAuthentication.Jwt do

{purpose, opts} = Keyword.pop(opts, :purpose, :user)

default_claims = Config.default_claims(resource, opts)
signer = Config.token_signer(resource, opts)

subject = AshAuthentication.user_to_subject(user)

extra_claims =
extra_claims
|> Map.put("sub", subject)

extra_claims =
{extra_claims, action_opts} =
case Map.fetch(user.__metadata__, :tenant) do
{:ok, tenant} -> Map.put(extra_claims, "tenant", to_string(tenant))
:error -> extra_claims
{:ok, tenant} ->
tenant = to_string(tenant)
{Map.put(extra_claims, "tenant", tenant), [tenant: tenant]}

:error ->
{extra_claims, opts}
end

default_claims = Config.default_claims(resource, action_opts)
signer = Config.token_signer(resource, opts)

with {:ok, token, claims} <- Joken.generate_and_sign(default_claims, extra_claims, signer),
:ok <- maybe_store_token(token, resource, user, purpose) do
:ok <- maybe_store_token(token, resource, user, purpose, action_opts) do
{:ok, token, claims}
else
{:error, _reason} -> :error
end
end

defp maybe_store_token(token, resource, user, purpose) do
defp maybe_store_token(token, resource, user, purpose, opts) do
if Info.authentication_tokens_store_all_tokens?(resource) do
with {:ok, token_resource} <- Info.authentication_tokens_token_resource(resource) do
TokenResource.Actions.store_token(
Expand All @@ -123,11 +127,11 @@ defmodule AshAuthentication.Jwt do
"token" => token,
"purpose" => to_string(purpose)
},
context: %{
Keyword.put(opts, :context, %{
ash_authentication: %{
user: user
}
}
})
)
end
else
Expand Down
12 changes: 7 additions & 5 deletions lib/ash_authentication/jwt/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ defmodule AshAuthentication.Jwt.Config do
|> Config.add_claim(
"jti",
&Joken.generate_jti/0,
&validate_jti/3
&validate_jti(&1, &2, &3, opts)
)
end

Expand Down Expand Up @@ -96,18 +96,20 @@ defmodule AshAuthentication.Jwt.Config do
resource. Requires that the subject's resource configuration be passed as the
validation context. This is automatically done by calling `Jwt.verify/2`.
"""
@spec validate_jti(String.t(), any, Resource.t() | any) :: boolean
def validate_jti(jti, _claims, resource) when is_atom(resource) do
@spec validate_jti(String.t(), any, Resource.t() | any, Keyword.t()) :: boolean
def validate_jti(jti, _claims, resource, opts \\ [])

def validate_jti(jti, _claims, resource, opts) when is_atom(resource) do
case Info.authentication_tokens_token_resource(resource) do
{:ok, token_resource} ->
TokenResource.Actions.valid_jti?(token_resource, jti)
TokenResource.Actions.valid_jti?(token_resource, jti, opts)

_ ->
false
end
end

def validate_jti(_, _, _), do: false
def validate_jti(_, _, _, _), do: false

@doc """
The signer used to sign the token on a per-resource basis.
Expand Down
9 changes: 8 additions & 1 deletion lib/ash_authentication/strategies/magic_link/actions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ defmodule AshAuthentication.Strategy.MagicLink.Actions do
{:ok, []} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
Expand All @@ -58,6 +59,7 @@ defmodule AshAuthentication.Strategy.MagicLink.Actions do
{:ok, _users} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
Expand All @@ -67,11 +69,16 @@ defmodule AshAuthentication.Strategy.MagicLink.Actions do
)}

{:error, error} when is_exception(error) ->
{:error, Errors.AuthenticationFailed.exception(caused_by: error)}
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: error
)}

{:error, error} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
Expand Down
9 changes: 8 additions & 1 deletion lib/ash_authentication/strategies/oauth2/actions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
{:ok, []} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
Expand All @@ -51,6 +52,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
{:ok, _users} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
Expand All @@ -63,11 +65,16 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
{:error, error}

{:error, error} when is_exception(error) ->
{:error, Errors.AuthenticationFailed.exception(caused_by: error)}
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: error
)}

{:error, error} ->
{:error,
Errors.AuthenticationFailed.exception(
strategy: strategy,
caused_by: %{
module: __MODULE__,
strategy: strategy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
:error ->
{:error,
AuthenticationFailed.exception(
strategy: :unknown,
query: query,
caused_by: %{
module: __MODULE__,
Expand All @@ -42,6 +43,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
_, _ ->
{:error,
AuthenticationFailed.exception(
strategy: strategy,
query: query,
caused_by: %{
module: __MODULE__,
Expand Down
44 changes: 25 additions & 19 deletions lib/ash_authentication/strategies/password.ex
Original file line number Diff line number Diff line change
Expand Up @@ -95,21 +95,24 @@ defmodule AshAuthentication.Strategy.Password do
#{Spark.Dsl.Extension.doc_entity(Dsl.dsl())}
"""

defstruct identity_field: :username,
hashed_password_field: :hashed_password_field,
defstruct confirmation_required?: false,
hash_provider: AshAuthentication.BcryptProvider,
confirmation_required?: false,
password_field: :password,
hashed_password_field: :hashed_password_field,
identity_field: :username,
name: nil,
password_confirmation_field: :password_confirmation,
password_field: :password,
provider: :password,
register_action_accept: [],
register_action_name: nil,
sign_in_action_name: nil,
registration_enabled?: true,
sign_in_enabled?: true,
resettable: [],
register_action_accept: [],
name: nil,
provider: :password,
resource: nil
resource: nil,
sign_in_action_name: nil,
sign_in_enabled?: true,
sign_in_token_lifetime: 60,
sign_in_tokens_enabled?: false,
sign_in_with_token_action_name: nil

alias Ash.Resource

Expand All @@ -125,21 +128,24 @@ defmodule AshAuthentication.Strategy.Password do
use Custom, entity: Dsl.dsl()

@type t :: %Password{
identity_field: atom,
hashed_password_field: atom,
hash_provider: module,
confirmation_required?: boolean,
password_field: atom,
hash_provider: module,
hashed_password_field: atom,
identity_field: atom,
name: atom,
password_confirmation_field: atom,
password_field: atom,
provider: atom,
register_action_accept: [atom],
register_action_name: atom,
sign_in_action_name: atom,
registration_enabled?: boolean,
sign_in_enabled?: boolean,
resettable: [Resettable.t()],
name: atom,
provider: atom,
resource: module
resource: module,
sign_in_action_name: atom,
sign_in_enabled?: boolean,
sign_in_token_lifetime: pos_integer,
sign_in_tokens_enabled?: boolean,
sign_in_with_token_action_name: atom
}

defdelegate dsl(), to: Dsl
Expand Down
Loading

0 comments on commit eca8cad

Please sign in to comment.