diff --git a/.formatter.exs b/.formatter.exs index 864fcc4..6bf07ed 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,7 +1,6 @@ spark_locals_without_parens = [ - access_token_attribute_name: 1, - access_token_expires_at_attribute_name: 1, api: 1, + auth0: 0, auth0: 1, auth0: 2, auth_method: 1, @@ -12,16 +11,17 @@ spark_locals_without_parens = [ confirm_action_name: 1, confirm_on_create?: 1, confirm_on_update?: 1, + confirmation: 0, confirmation: 1, confirmation: 2, confirmation_required?: 1, confirmed_at_field: 1, - destroy_action_name: 1, enabled?: 1, expunge_expired_action_name: 1, expunge_interval: 1, get_by_subject_action_name: 1, get_changes_action_name: 1, + get_token_action_name: 1, hash_provider: 1, hashed_password_field: 1, identity_field: 1, @@ -31,21 +31,22 @@ spark_locals_without_parens = [ inhibit_updates?: 1, is_revoked_action_name: 1, monitor_fields: 1, + oauth2: 0, oauth2: 1, oauth2: 2, + password: 0, password: 1, password: 2, password_confirmation_field: 1, password_field: 1, password_reset_action_name: 1, private_key: 1, - read_action_name: 1, read_expired_action_name: 1, redirect_uri: 1, - refresh_token_attribute_name: 1, register_action_name: 1, registration_enabled?: 1, request_password_reset_action_name: 1, + require_token_presence_for_authentication?: 1, resettable: 0, resettable: 1, revoke_token_action_name: 1, @@ -57,17 +58,11 @@ spark_locals_without_parens = [ store_all_tokens?: 1, store_changes_action_name: 1, store_token_action_name: 1, - strategy_attribute_name: 1, subject_name: 1, token_lifetime: 1, token_path: 1, token_resource: 1, - uid_attribute_name: 1, - upsert_action_name: 1, - user_id_attribute_name: 1, - user_path: 1, - user_relationship_name: 1, - user_resource: 1 + user_path: 1 ] [ diff --git a/lib/ash_authentication.ex b/lib/ash_authentication.ex index 1d6583c..9a1ce18 100644 --- a/lib/ash_authentication.ex +++ b/lib/ash_authentication.ex @@ -129,7 +129,7 @@ defmodule AshAuthentication do ## Example iex> authenticated_resources(:ash_authentication) - [Example.User] + [Example.User, Example.UserWithTokenRequired] """ @spec authenticated_resources(atom) :: [Resource.t()] diff --git a/lib/ash_authentication/dsl.ex b/lib/ash_authentication/dsl.ex index fc7effb..b694b94 100644 --- a/lib/ash_authentication/dsl.ex +++ b/lib/ash_authentication/dsl.ex @@ -129,6 +129,20 @@ defmodule AshAuthentication.Dsl do """, default: false ], + require_token_presence_for_authentication?: [ + type: :boolean, + doc: """ + Require a locally-stored token for authentication? + + This inverts the token validation behaviour from requiring that + tokens are not revoked to requiring any token presented by a + client to be present in the token resource to be considered + valid. + + Requires `store_all_tokens?` to be `true`. + """, + default: false + ], signing_algorithm: [ type: :string, doc: """ diff --git a/lib/ash_authentication/plug/helpers.ex b/lib/ash_authentication/plug/helpers.ex index 4e7456f..ab25bcd 100644 --- a/lib/ash_authentication/plug/helpers.ex +++ b/lib/ash_authentication/plug/helpers.ex @@ -13,9 +13,13 @@ defmodule AshAuthentication.Plug.Helpers do @spec store_in_session(Conn.t(), Resource.record()) :: Conn.t() def store_in_session(conn, user) when is_struct(user) do subject_name = Info.authentication_subject_name!(user.__struct__) - subject = AshAuthentication.user_to_subject(user) - Conn.put_session(conn, subject_name, subject) + if Info.authentication_tokens_require_token_presence_for_authentication?(user.__struct__) do + Conn.put_session(conn, "#{subject_name}_token", user.__metadata__.token) + else + subject = AshAuthentication.user_to_subject(user) + Conn.put_session(conn, subject_name, subject) + end end def store_in_session(conn, _), do: conn @@ -60,17 +64,39 @@ defmodule AshAuthentication.Plug.Helpers do def retrieve_from_session(conn, otp_app) do otp_app |> AshAuthentication.authenticated_resources() - |> Stream.map(&{&1, Info.authentication_options(&1)}) - |> Enum.reduce(conn, fn {resource, options}, conn -> - current_subject_name = current_subject_name(options.subject_name) + |> Stream.map( + &{&1, Info.authentication_options(&1), + Info.authentication_tokens_require_token_presence_for_authentication?(&1)} + ) + |> Enum.reduce(conn, fn + {resource, options, true}, conn -> + current_subject_name = current_subject_name(options.subject_name) + token_resource = Info.authentication_tokens_token_resource!(resource) - with subject when is_binary(subject) <- Conn.get_session(conn, options.subject_name), - {:ok, user} <- AshAuthentication.subject_to_user(subject, resource) do - Conn.assign(conn, current_subject_name, user) - else - _ -> - Conn.assign(conn, current_subject_name, nil) - end + with token when is_binary(token) <- + Conn.get_session(conn, "#{options.subject_name}_token"), + {:ok, %{"sub" => subject, "jti" => jti}, _} <- Jwt.verify(token, otp_app), + {:ok, [_]} <- + TokenResource.Actions.get_token(token_resource, %{ + "jti" => jti, + "purpose" => "generic" + }), + {:ok, user} <- AshAuthentication.subject_to_user(subject, resource) do + Conn.assign(conn, current_subject_name, user) + else + _ -> Conn.assign(conn, current_subject_name, nil) + end + + {resource, options, false}, conn -> + current_subject_name = current_subject_name(options.subject_name) + + with subject when is_binary(subject) <- Conn.get_session(conn, options.subject_name), + {:ok, user} <- AshAuthentication.subject_to_user(subject, resource) do + Conn.assign(conn, current_subject_name, user) + else + _ -> + Conn.assign(conn, current_subject_name, nil) + end end) end @@ -90,7 +116,8 @@ defmodule AshAuthentication.Plug.Helpers do |> Stream.filter(&String.starts_with?(&1, "Bearer ")) |> Stream.map(&String.replace_leading(&1, "Bearer ", "")) |> Enum.reduce(conn, fn token, conn -> - with {:ok, %{"sub" => subject}, resource} <- Jwt.verify(token, otp_app), + with {:ok, %{"sub" => subject, "jti" => jti}, resource} <- Jwt.verify(token, otp_app), + :ok <- validate_token(resource, jti), {:ok, user} <- AshAuthentication.subject_to_user(subject, resource), {:ok, subject_name} <- Info.authentication_subject_name(resource), current_subject_name <- current_subject_name(subject_name) do @@ -102,6 +129,21 @@ defmodule AshAuthentication.Plug.Helpers do end) end + defp validate_token(resource, jti) do + if Info.authentication_tokens_require_token_presence_for_authentication?(resource) do + with {:ok, token_resource} <- Info.authentication_tokens_token_resource(resource), + {:ok, [_]} <- + TokenResource.Actions.get_token(token_resource, %{ + "jti" => jti, + "purpose" => "generic" + }) do + :ok + end + else + :ok + end + end + @doc """ Revoke all authorization header(s). diff --git a/lib/ash_authentication/token_resource.ex b/lib/ash_authentication/token_resource.ex index 095bcec..3d73889 100644 --- a/lib/ash_authentication/token_resource.ex +++ b/lib/ash_authentication/token_resource.ex @@ -44,9 +44,18 @@ defmodule AshAuthentication.TokenResource do doc: """ The name of the action to use to store a token. - Used it `store_all_tokens?` is enabled in your authentication resource. + Used if `store_all_tokens?` is enabled in your authentication resource. """, default: :store_token + ], + get_token_action_name: [ + type: :atom, + doc: """ + The name of the action used to retrieve tokens from the store. + + Used if `require_token_presence_for_authentication?` is enabled in your authentication resource. + """, + default: :get_token ] ], sections: [ diff --git a/lib/ash_authentication/token_resource/actions.ex b/lib/ash_authentication/token_resource/actions.ex index 6354adb..b59229e 100644 --- a/lib/ash_authentication/token_resource/actions.ex +++ b/lib/ash_authentication/token_resource/actions.ex @@ -102,7 +102,7 @@ defmodule AshAuthentication.TokenResource.Actions do @doc """ Revoke a token. - Extracts the JTI from the provided token and uses it to generate a revocationr + Extracts the JTI from the provided token and uses it to generate a revocation record. """ @spec revoke(Resource.t(), String.t(), keyword) :: :ok | {:error, any} @@ -149,6 +149,20 @@ defmodule AshAuthentication.TokenResource.Actions do end end + @doc """ + Retrieve a token by token or JTI optionally filtering by purpose. + """ + @spec get_token(Resource.t(), map, keyword) :: {:ok, [Resource.record()]} | {:error, any} + def get_token(resource, params, opts \\ []) do + with :ok <- assert_resource_has_extension(resource, TokenResource), + {:ok, api} <- Info.token_api(resource), + {:ok, get_token_action_name} <- Info.token_get_token_action_name(resource) do + resource + |> Query.for_read(get_token_action_name, params, opts) + |> api.read() + end + end + defp expunge_inside_transaction(resource, expunge_expired_action_name, opts) do with :ok <- assert_resource_has_extension(resource, TokenResource), {:ok, api} <- Info.token_api(resource), diff --git a/lib/ash_authentication/token_resource/get_token_preparation.ex b/lib/ash_authentication/token_resource/get_token_preparation.ex new file mode 100644 index 0000000..6703ee4 --- /dev/null +++ b/lib/ash_authentication/token_resource/get_token_preparation.ex @@ -0,0 +1,49 @@ +defmodule AshAuthentication.TokenResource.GetTokenPreparation do + @moduledoc """ + Constrains a query to only records which match the `jti` or `token` argument + and optionally by the `purpose` argument. + """ + + use Ash.Resource.Preparation + alias Ash.{Error.Query.InvalidArgument, Query, Resource.Preparation} + alias AshAuthentication.Jwt + require Ash.Query + + @doc false + @impl true + @spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t() + def prepare(query, _, _) do + jti = get_jti(query) + purpose = Query.get_argument(query, :purpose) + + query + |> Query.filter(jti: jti) + |> then(fn query -> + if purpose, do: Query.filter(query, purpose: purpose), else: query + end) + |> Query.filter(expires_at > now()) + end + + defp get_jti(query), + do: get_jti(Query.get_argument(query, :jti), Query.get_argument(query, :token)) + + defp get_jti(jti, _token) when byte_size(jti) > 0, do: jti + + defp get_jti(_jti, token) when byte_size(token) > 0 do + token + |> Jwt.peek() + |> case do + {:ok, %{"jti" => jti}} -> jti + _ -> get_jti(nil, nil) + end + end + + defp get_jti(_jti, _token), + do: + raise( + InvalidArgument.exception( + field: :jti, + message: "At least one of `jti` or `token` arguments must be present" + ) + ) +end diff --git a/lib/ash_authentication/token_resource/transformer.ex b/lib/ash_authentication/token_resource/transformer.ex index 690ef03..33e3d99 100644 --- a/lib/ash_authentication/token_resource/transformer.ex +++ b/lib/ash_authentication/token_resource/transformer.ex @@ -140,11 +140,66 @@ defmodule AshAuthentication.TokenResource.Transformer do store_token_action_name, &build_store_token_action(&1, store_token_action_name) ), - :ok <- validate_store_token_action(dsl_state, store_token_action_name) do + :ok <- validate_store_token_action(dsl_state, store_token_action_name), + {:ok, get_token_action_name} <- Info.token_get_token_action_name(dsl_state), + {:ok, dsl_state} <- + maybe_build_action( + dsl_state, + get_token_action_name, + &build_get_token_action(&1, get_token_action_name) + ), + :ok <- validate_get_token_action(dsl_state, get_token_action_name) do {:ok, dsl_state} end end + defp validate_get_token_action(dsl_state, action_name) do + with {:ok, action} <- validate_action_exists(dsl_state, action_name), + :ok <- + validate_action_argument_option(action, :token, :type, [Ash.Type.String, :string]), + :ok <- validate_action_argument_option(action, :token, :allow_nil?, [true]), + :ok <- validate_action_argument_option(action, :token, :sensitive?, [true]), + :ok <- validate_action_argument_option(action, :jti, :type, [Ash.Type.String, :string]), + :ok <- validate_action_argument_option(action, :jti, :allow_nil?, [true]), + :ok <- + validate_action_argument_option(action, :purpose, :type, [Ash.Type.String, :string]) do + validate_action_has_preparation(action, TokenResource.GetTokenPreparation) + end + end + + defp build_get_token_action(_dsl_state, action_name) do + arguments = [ + Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument, + name: :token, + type: :string, + sensitive?: true + ), + Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument, + name: :jti, + type: :string, + sensitive?: false + ), + Transformer.build_entity!(Resource.Dsl, [:actions, :read], :argument, + name: :purpose, + type: :string, + sensitive?: false + ) + ] + + preparations = [ + Transformer.build_entity!(Resource.Dsl, [:actions, :read], :prepare, + preparation: TokenResource.GetTokenPreparation + ) + ] + + Transformer.build_entity(Resource.Dsl, [:actions], :read, + name: action_name, + arguments: arguments, + preparations: preparations, + get?: true + ) + end + defp validate_store_token_action(dsl_state, action_name) do with {:ok, action} <- validate_action_exists(dsl_state, action_name), :ok <- validate_token_argument(action) do diff --git a/lib/ash_authentication/utils.ex b/lib/ash_authentication/utils.ex index 80325ee..f114d57 100644 --- a/lib/ash_authentication/utils.ex +++ b/lib/ash_authentication/utils.ex @@ -215,11 +215,10 @@ defmodule AshAuthentication.Utils do """ @spec assert_is_module(module) :: :ok | {:error, term} def assert_is_module(module) when is_atom(module) do - module.module_info() - :ok - rescue - _ -> - {:error, "Argument `#{inspect(module)}` is not a valid module"} + case Code.ensure_compiled(module) do + {:module, _} -> :ok + _ -> {:error, "Argument `#{inspect(module)}` is not a valid module"} + end end def assert_is_module(module), diff --git a/lib/ash_authentication/verifier.ex b/lib/ash_authentication/verifier.ex index 4e317db..ef2e9ca 100644 --- a/lib/ash_authentication/verifier.ex +++ b/lib/ash_authentication/verifier.ex @@ -18,6 +18,11 @@ defmodule AshAuthentication.Verifier do @spec before?(any) :: boolean def before?(_), do: false + @doc false + @impl true + @spec after_compile? :: boolean + def after_compile?, do: true + @doc false @impl true @spec transform(map) :: diff --git a/priv/repo/migrations/20230111010206_add_user_with_token_required_resource.exs b/priv/repo/migrations/20230111010206_add_user_with_token_required_resource.exs new file mode 100644 index 0000000..3e6efc2 --- /dev/null +++ b/priv/repo/migrations/20230111010206_add_user_with_token_required_resource.exs @@ -0,0 +1,31 @@ +defmodule Example.Repo.Migrations.AddUserWithTokenRequiredResource do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + create table(:user_with_token_required, primary_key: false) do + add :id, :uuid, null: false, default: fragment("uuid_generate_v4()"), primary_key: true + add :email, :citext, null: false + add :hashed_password, :text + add :created_at, :utc_datetime_usec, null: false, default: fragment("now()") + add :updated_at, :utc_datetime_usec, null: false, default: fragment("now()") + end + + create unique_index(:user_with_token_required, [:email], + name: "user_with_token_required_email_index" + ) + end + + def down do + drop_if_exists unique_index(:user_with_token_required, [:email], + name: "user_with_token_required_email_index" + ) + + drop table(:user_with_token_required) + end +end \ No newline at end of file diff --git a/priv/resource_snapshots/repo/user_with_token_required/20230111010206.json b/priv/resource_snapshots/repo/user_with_token_required/20230111010206.json new file mode 100644 index 0000000..12770af --- /dev/null +++ b/priv/resource_snapshots/repo/user_with_token_required/20230111010206.json @@ -0,0 +1,78 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v4()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "email", + "type": "citext" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "hashed_password", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"now()\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "created_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"now()\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "601DA745C121A871D5428D2BBC269B22E7D92FBE4D0111B5468649E999F656AA", + "identities": [ + { + "base_filter": null, + "index_name": "user_with_token_required_email_index", + "keys": [ + "email" + ], + "name": "email" + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Example.Repo", + "schema": null, + "table": "user_with_token_required" +} \ No newline at end of file diff --git a/test/ash_authentication/plug/helpers_test.exs b/test/ash_authentication/plug/helpers_test.exs index 2fe2ba4..a5c8a39 100644 --- a/test/ash_authentication/plug/helpers_test.exs +++ b/test/ash_authentication/plug/helpers_test.exs @@ -1,7 +1,7 @@ defmodule AshAuthentication.Plug.HelpersTest do @moduledoc false use DataCase, async: true - alias AshAuthentication.{Jwt, Plug.Helpers} + alias AshAuthentication.{Jwt, Plug.Helpers, TokenResource} import Plug.Test, only: [conn: 3] alias Plug.Conn @@ -15,7 +15,7 @@ defmodule AshAuthentication.Plug.HelpersTest do end describe "store_in_session/2" do - test "it stores the user in the session", %{conn: conn} do + test "when token presence is not required it stores the user in the session", %{conn: conn} do user = build_user() subject = AshAuthentication.user_to_subject(user) @@ -25,6 +25,17 @@ defmodule AshAuthentication.Plug.HelpersTest do assert conn.private.plug_session["user"] == subject end + + test "when token presence is required it stores the token in the session", %{conn: conn} do + user = build_user_with_token_required() + + conn = + conn + |> Helpers.store_in_session(user) + + assert conn.private.plug_session["user_with_token_required_token"] == + user.__metadata__.token + end end describe "load_subjects/2" do @@ -39,7 +50,9 @@ defmodule AshAuthentication.Plug.HelpersTest do end describe "retrieve_from_session/2" do - test "it loads any subjects stored in the session", %{conn: conn} do + test "when token presence is not required it loads any subjects stored in the session", %{ + conn: conn + } do user = build_user() subject = AshAuthentication.user_to_subject(user) @@ -50,10 +63,54 @@ defmodule AshAuthentication.Plug.HelpersTest do assert conn.assigns.current_user.id == user.id end + + test "when token presence is required and the token is present in the token resource it loads the token's subject", + %{conn: conn} do + user = build_user_with_token_required() + + conn = + conn + |> Conn.put_session("user_with_token_required_token", user.__metadata__.token) + |> Helpers.retrieve_from_session(:ash_authentication) + + assert conn.assigns.current_user_with_token_required.id == user.id + end + + test "when token presense is required and the token is not present in the token resource it doesn't load the token's subject", + %{conn: conn} do + user = build_user_with_token_required() + {:ok, %{"jti" => jti}} = Jwt.peek(user.__metadata__.token) + + import Ecto.Query + + Example.Repo.delete_all(from(t in Example.Token, where: t.jti == ^jti)) + + conn = + conn + |> Conn.put_session("user_with_token_required_token", user.__metadata__.token) + |> Helpers.retrieve_from_session(:ash_authentication) + + refute conn.assigns.current_user_with_token_required + end + + test "when token presense is requried and the token has been revoked it doesn't load the token's subject", + %{conn: conn} do + user = build_user_with_token_required() + + :ok = TokenResource.revoke(Example.Token, user.__metadata__.token) + + conn = + conn + |> Conn.put_session("user_with_token_required_token", user.__metadata__.token) + |> Helpers.retrieve_from_session(:ash_authentication) + + refute conn.assigns.current_user_with_token_required + end end describe "retrieve_from_bearer/2" do - test "it loads any subjects from authorization headers", %{conn: conn} do + test "when token presense is not required it loads any subjects from authorization header(s)", + %{conn: conn} do user = build_user() conn = @@ -63,6 +120,49 @@ defmodule AshAuthentication.Plug.HelpersTest do assert conn.assigns.current_user.id == user.id end + + test "when token presense is required and the token is present in the database it loads the subjects from the authorization header(s)", + %{conn: conn} do + user = build_user_with_token_required() + + conn = + conn + |> Conn.put_req_header("authorization", "Bearer #{user.__metadata__.token}") + |> Helpers.retrieve_from_bearer(:ash_authentication) + + assert conn.assigns.current_user_with_token_required.id == user.id + end + + test "when token presense is required and the token is not present in the token resource it doesn't load the subjects from the authorization header(s)", + %{conn: conn} do + user = build_user_with_token_required() + {:ok, %{"jti" => jti}} = Jwt.peek(user.__metadata__.token) + + import Ecto.Query + + Example.Repo.delete_all(from(t in Example.Token, where: t.jti == ^jti)) + + conn = + conn + |> Conn.put_req_header("authorization", "Bearer #{user.__metadata__.token}") + |> Helpers.retrieve_from_bearer(:ash_authentication) + + refute is_map_key(conn.assigns, :current_user_with_token_required) + end + + test "when token presense is required and the token has been revoked it doesn't lkoad the subjects from the authorization header(s)", + %{conn: conn} do + user = build_user_with_token_required() + + :ok = TokenResource.revoke(Example.Token, user.__metadata__.token) + + conn = + conn + |> Conn.put_req_header("authorization", "Bearer #{user.__metadata__.token}") + |> Helpers.retrieve_from_bearer(:ash_authentication) + + refute is_map_key(conn.assigns, :current_user_with_token_required) + end end describe "revoke_bearer_tokens/2" do diff --git a/test/ash_authentication_test.exs b/test/ash_authentication_test.exs index 89d7c24..34c8787 100644 --- a/test/ash_authentication_test.exs +++ b/test/ash_authentication_test.exs @@ -6,7 +6,8 @@ defmodule AshAuthenticationTest do describe "authenticated_resources/0" do test "it correctly locates all authenticatable resources" do - assert [Example.User] = authenticated_resources(:ash_authentication) + assert [Example.User, Example.UserWithTokenRequired] = + authenticated_resources(:ash_authentication) end end end diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 3f4be01..b0ed7ba 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -90,4 +90,28 @@ defmodule DataCase do Ash.Resource.put_metadata(user, field, value) end) end + + @doc "User with token required factory" + @spec build_user_with_token_required(keyword) :: Example.UserWithTokenRequired.t() | no_return + def build_user_with_token_required(attrs \\ []) do + password = password() + + attrs = + attrs + |> Map.new() + |> Map.put_new(:email, Faker.Internet.email()) + |> Map.put_new(:password, password) + |> Map.put_new(:password_confirmation, password) + + user = + Example.UserWithTokenRequired + |> Ash.Changeset.new() + |> Ash.Changeset.for_create(:register_with_password, attrs) + |> Example.create!() + + attrs + |> Enum.reduce(user, fn {field, value}, user -> + Ash.Resource.put_metadata(user, field, value) + end) + end end diff --git a/test/support/example/auth_plug.ex b/test/support/example/auth_plug.ex index a23242c..e6da160 100644 --- a/test/support/example/auth_plug.ex +++ b/test/support/example/auth_plug.ex @@ -22,10 +22,7 @@ defmodule Example.AuthPlug do Jason.encode!(%{ status: :success, token: token, - user: %{ - id: user.id, - username: user.username - }, + user: Map.take(user, ~w[username id email]a), strategy: strategy, phase: phase }) diff --git a/test/support/example/registry.ex b/test/support/example/registry.ex index af64bd2..0cd130d 100644 --- a/test/support/example/registry.ex +++ b/test/support/example/registry.ex @@ -4,6 +4,7 @@ defmodule Example.Registry do entries do entry Example.User + entry Example.UserWithTokenRequired entry Example.Token entry Example.UserIdentity end diff --git a/test/support/example/user_with_token_required.ex b/test/support/example/user_with_token_required.ex new file mode 100644 index 0000000..752c6d4 --- /dev/null +++ b/test/support/example/user_with_token_required.ex @@ -0,0 +1,60 @@ +defmodule Example.UserWithTokenRequired do + @moduledoc false + use Ash.Resource, data_layer: AshPostgres.DataLayer, extensions: [AshAuthentication] + + @type t :: %__MODULE__{ + id: Ecto.UUID.t(), + email: String.t(), + hashed_password: String.t(), + created_at: DateTime.t(), + updated_at: DateTime.t() + } + + attributes do + uuid_primary_key :id, writable?: true + attribute :email, :ci_string, allow_nil?: false + attribute :hashed_password, :string, allow_nil?: true, sensitive?: true, private?: true + create_timestamp :created_at + update_timestamp :updated_at + end + + authentication do + api Example + + tokens do + enabled? true + store_all_tokens? true + require_token_presence_for_authentication? true + token_resource Example.Token + signing_secret &get_config/2 + end + + strategies do + password do + identity_field :email + end + end + end + + actions do + defaults [:create, :read, :update, :destroy] + end + + identities do + identity :email, [:email], eager_check_with: Example + end + + postgres do + table "user_with_token_required" + repo(Example.Repo) + end + + def get_config(path, _resource) do + value = + :ash_authentication + |> Application.get_all_env() + |> get_in(path) + + {:ok, value} + end +end