diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..6f03741 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,3 @@ +[ + inputs: ["mix.exs", "{config,lib,test,priv}/**/*.{ex,exs}"] +] diff --git a/README.md b/README.md index e94ef00..d384480 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,10 @@ run `mix guardian_db.gen.migration` to generate a migration. sweep_interval: 60 # default: 60 minutes ``` -To sweep expired tokens from your db you should add `Guardian.DB.ExpiredSweeper` to your supervision tree. +To sweep expired tokens from your db you should add `Guardian.DB.Token.SweeperServer` to your supervision tree. ```elixir - worker(Guardian.DB.ExpiredSweeper, []) + worker(Guardian.DB.Token.SweeperServer, []) ``` `Guardian.DB` works by hooking into the lifecycle of your token module. diff --git a/config/config.exs b/config/config.exs index 3072f4d..a097f47 100644 --- a/config/config.exs +++ b/config/config.exs @@ -2,9 +2,7 @@ use Mix.Config config :guardian, Guardian.DB, issuer: "GuardianDB", - secret_key: "HcdlxxmyDRvfrwdpjUPh2M8mWP+KtpOQK1g6fT5SHrnflSY8KiWeORqN6IZSJYTA" - -config :guardian, Guardian.DB, + secret_key: "HcdlxxmyDRvfrwdpjUPh2M8mWP+KtpOQK1g6fT5SHrnflSY8KiWeORqN6IZSJYTA", repo: Guardian.DB.Test.Repo config :guardian_db, ecto_repos: [Guardian.DB.Test.Repo] diff --git a/lib/guardian/db.ex b/lib/guardian/db.ex index d576077..e07760d 100644 --- a/lib/guardian/db.ex +++ b/lib/guardian/db.ex @@ -45,15 +45,14 @@ defmodule Guardian.DB do ```elixir create table(:guardian_tokens, primary_key: false) do - add :jti, :string, primary_key: true - add :typ, :string - add :aud, :string - add :iss, :string - add :sub, :string - add :exp, :bigint - add :jwt, :text - add :claims, :map - + add(:jti, :string, primary_key: true) + add(:typ, :string) + add(:aud, :string) + add(:iss, :string) + add(:sub, :string) + add(:exp, :bigint) + add(:jwt, :text) + add(:claims, :map) timestamps() end ``` @@ -119,7 +118,7 @@ defmodule Guardian.DB do After the JWT is generated, stores the various fields of it in the DB for tracking """ def after_encode_and_sign(resource, type, claims, jwt) do - case Token.create!(claims, jwt) do + case Token.create(claims, jwt) do {:error, _} -> {:error, :token_storage_failure} _ -> {:ok, {resource, type, claims, jwt}} end diff --git a/lib/guardian/db/expired_sweeper.ex b/lib/guardian/db/expired_sweeper.ex deleted file mode 100644 index 01ef7a0..0000000 --- a/lib/guardian/db/expired_sweeper.ex +++ /dev/null @@ -1,88 +0,0 @@ -defmodule Guardian.DB.ExpiredSweeper do - @moduledoc """ - Periocially purges expired tokens from the DB. - - ## Example - config :guardian, Guardian.DB, - sweep_interval: 60 # 1 hour - - # in your supervisor - worker(Guardian.DB.ExpiredSweeper, []) - """ - use GenServer - - alias Guardian.DB.Token - - def start_link, do: start_link([]) - - def start_link(state, _opts \\ []) do - state = Enum.into(state, %{}) - - GenServer.start_link(__MODULE__, state, name: __MODULE__) - end - - @doc """ - Reset the purge timer. - """ - def reset_timer! do - GenServer.call(__MODULE__, :reset_timer) - end - - @doc """ - Manually trigger a db purge of expired tokens. - Also resets the current timer. - """ - def purge! do - GenServer.call(__MODULE__, :sweep) - end - - def init(state) do - {:ok, reset_state_timer!(state)} - end - - def handle_call(:reset_timer, _from, state) do - {:reply, :ok, reset_state_timer!(state)} - end - - def handle_call(:sweep, _from, state) do - {:reply, :ok, sweep!(state)} - end - - def handle_info(:sweep, state) do - {:noreply, sweep!(state)} - end - - def handle_info(_, state) do - {:ok, state} - end - - defp sweep!(state) do - Token.purge_expired_tokens!() - reset_state_timer!(state) - end - - defp reset_state_timer!(state) do - if state[:timer] do - Process.cancel_timer(state.timer) - end - - timer = Process.send_after(self(), :sweep, interval()) - Map.merge(state, %{timer: timer}) - end - - defp interval do - :guardian - |> Application.get_env(Guardian.DB) - |> Keyword.get(:sweep_interval, 60) - |> minute_to_ms - end - - defp minute_to_ms(value) when is_binary(value) do - value - |> String.to_integer() - |> minute_to_ms - end - - defp minute_to_ms(value) when value < 1, do: 1000 - defp minute_to_ms(value), do: round(value * 60 * 1000) -end diff --git a/lib/guardian/db/token.ex b/lib/guardian/db/token.ex index 1c32691..3b3eda5 100644 --- a/lib/guardian/db/token.ex +++ b/lib/guardian/db/token.ex @@ -37,7 +37,7 @@ defmodule Guardian.DB.Token do @doc """ Create a new new token based on the JWT and decoded claims """ - def create!(claims, jwt) do + def create(claims, jwt) do prepared_claims = claims |> Map.put("jwt", jwt) @@ -52,7 +52,7 @@ defmodule Guardian.DB.Token do @doc """ Purge any tokens that are expired. This should be done periodically to keep your DB table clean of clutter """ - def purge_expired_tokens! do + def purge_expired_tokens do timestamp = Guardian.timestamp() query_schema() @@ -60,10 +60,12 @@ defmodule Guardian.DB.Token do |> Guardian.DB.repo().delete_all() end + @doc false def query_schema do {schema_name(), Token} end + @doc false def schema_name do :guardian |> Application.fetch_env!(Guardian.DB) diff --git a/lib/guardian/db/token/sweeper.ex b/lib/guardian/db/token/sweeper.ex new file mode 100644 index 0000000..dd8065e --- /dev/null +++ b/lib/guardian/db/token/sweeper.ex @@ -0,0 +1,44 @@ +defmodule Guardian.DB.Token.Sweeper do + @moduledoc """ + Purges and schedule work for cleaning up expired tokens. + """ + + alias Guardian.DB.Token + + @doc """ + Purges the expired tokens and schedule the next purge. + """ + def sweep(pid, state) do + Token.purge_expired_tokens() + schedule_work(pid, state) + end + + @doc """ + Schedule the next purge. + """ + def schedule_work(pid, state) do + if state[:timer] do + Process.cancel_timer(state.timer) + end + + timer = Process.send_after(pid, :sweep, state[:interval]) + Map.merge(state, %{timer: timer}) + end + + @doc false + def get_interval do + :guardian + |> Application.get_env(Guardian.DB) + |> Keyword.get(:sweep_interval, 60) + |> minute_to_ms() + end + + defp minute_to_ms(value) when is_binary(value) do + value + |> String.to_integer() + |> minute_to_ms() + end + + defp minute_to_ms(value) when value < 1, do: 1000 + defp minute_to_ms(value), do: round(value * 60 * 1000) +end diff --git a/lib/guardian/db/token/sweeper_server.ex b/lib/guardian/db/token/sweeper_server.ex new file mode 100644 index 0000000..6cfca3f --- /dev/null +++ b/lib/guardian/db/token/sweeper_server.ex @@ -0,0 +1,58 @@ +defmodule Guardian.DB.Token.SweeperServer do + @moduledoc """ + Periocially purges expired tokens from the DB. + + ## Example + config :guardian, Guardian.DB, + sweep_interval: 60 # 1 hour + + # in your supervisor + worker(Guardian.DB.Token.SweeperServer, []) + """ + + use GenServer + alias Guardian.DB.Token.Sweeper + + def start_link(opts \\ []) do + defaults = %{ + interval: Sweeper.get_interval() + } + + state = Enum.into(opts, defaults) + + GenServer.start_link(__MODULE__, state, name: __MODULE__) + end + + @doc """ + Reset the purge timer. + """ + def reset_timer do + GenServer.call(__MODULE__, :reset_timer) + end + + @doc """ + Manually trigger a database purge of expired tokens. Also resets the current + scheduled work. + """ + def purge do + GenServer.call(__MODULE__, :sweep) + end + + def init(state) do + {:ok, Sweeper.schedule_work(self(), state)} + end + + def handle_call(:reset_timer, _from, state) do + {:reply, :ok, Sweeper.schedule_work(self(), state)} + end + + def handle_call(:sweep, _from, state) do + {:reply, :ok, Sweeper.sweep(self(), state)} + end + + def handle_info(:sweep, state) do + {:noreply, Sweeper.sweep(self(), state)} + end + + def handle_info(_, state), do: {:noreply, state} +end diff --git a/mix.exs b/mix.exs index da8f804..7643199 100644 --- a/mix.exs +++ b/mix.exs @@ -2,19 +2,24 @@ defmodule Guardian.DB.Mixfile do use Mix.Project @version "1.0.0" + @source_url "https://github.com/ueberauth/guardian_db" def project do - [app: :guardian_db, - version: @version, - description: "DB tracking for token validity", - elixir: "~> 1.4 or ~> 1.5", - elixirc_paths: elixirc_paths(Mix.env), - package: package(), - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - preferred_cli_env: [guardian_db: :test], - aliases: aliases(), - deps: deps()] + [ + name: "Guardian.DB", + app: :guardian_db, + version: @version, + description: "DB tracking for token validity", + elixir: "~> 1.4 or ~> 1.5", + elixirc_paths: elixirc_paths(Mix.env()), + package: package(), + docs: docs(), + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + preferred_cli_env: [guardian_db: :test], + aliases: aliases(), + deps: deps() + ] end def application do @@ -22,26 +27,45 @@ defmodule Guardian.DB.Mixfile do end defp elixirc_paths(:test), do: ["lib", "test/support"] - defp elixirc_paths(_), do: ["lib"] + defp elixirc_paths(_), do: ["lib"] defp deps do - [{:guardian, "~> 1.0"}, - {:ecto, "~> 2.2"}, - {:postgrex, "~> 0.13", optional: true}, - {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, - {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}] + [ + {:guardian, "~> 1.0"}, + {:ecto, "~> 2.2"}, + {:postgrex, "~> 0.13", optional: true}, + {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, + {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false} + ] end defp package do [ maintainers: ["Daniel Neighman", "Sean Callan", "Sonny Scroggin"], licenses: ["MIT"], - links: %{github: "https://github.com/hassox/guardian_db"}, - files: ~w(lib) ++ ~w(CHANGELOG.md LICENSE mix.exs README.md) + links: %{GitHub: @source_url}, + files: [ + "lib", + "CHANGELOG.md", + "LICENSE", + "mix.exs", + "README.md", + "priv/templates" + ] + ] + end + + defp docs do + [ + main: "readme", + homepage_url: @source_url, + source_ref: "v#{@version}", + source_url: @source_url, + extras: ["README.md"] ] end defp aliases do - ["test": ["ecto.create --quiet", "ecto.migrate", "test"]] + [test: ["ecto.create --quiet", "ecto.migrate", "test"]] end end diff --git a/priv/test/migrations/20160929125415_migrations.exs b/priv/test/migrations/20160929125415_migrations.exs index e4407ef..5742d85 100644 --- a/priv/test/migrations/20160929125415_migrations.exs +++ b/priv/test/migrations/20160929125415_migrations.exs @@ -3,20 +3,20 @@ defmodule Guardian.DB.Test.Repo.Migrations do def up do create_if_not_exists table(:guardian_tokens, primary_key: false) do - add :jti, :string, primary_key: true - add :typ, :string - add :aud, :string - add :iss, :string - add :sub, :string - add :exp, :bigint - add :jwt, :text - add :claims, :map + add(:jti, :string, primary_key: true) + add(:typ, :string) + add(:aud, :string) + add(:iss, :string) + add(:sub, :string) + add(:exp, :bigint) + add(:jwt, :text) + add(:claims, :map) timestamps() end end def down do - drop table(:guardian_tokens) + drop(table(:guardian_tokens)) end end diff --git a/test/guardian/db_test.exs b/test/guardian/db_test.exs index d4e04ab..3288299 100644 --- a/test/guardian/db_test.exs +++ b/test/guardian/db_test.exs @@ -1,20 +1,17 @@ -defmodule Guardian.DBTest do +defmodule Guardian.DB.Test do use Guardian.DB.Test.DataCase alias Guardian.DB.Token - defp get_token(token_id \\ "token-uuid"), do: Repo.get(Token.query_schema(), token_id) - setup do {:ok, %{ - claims: %{ - "jti" => "token-uuid", - "aud" => "token", - "sub" => "the_subject", - "iss" => "the_issuer", - "exp" => Guardian.timestamp() + 1_000_000_000, - } + claims: %{ + "jti" => "token-uuid", + "aud" => "token", + "sub" => "the_subject", + "iss" => "the_issuer", + "exp" => Guardian.timestamp() + 1_000_000_000 } - } + }} end test "after_encode_and_sign_in is successful", context do @@ -35,7 +32,7 @@ defmodule Guardian.DBTest do end test "on_verify with a record in the db", context do - Token.create! context.claims, "The JWT" + Token.create(context.claims, "The JWT") token = get_token() assert token != nil @@ -55,7 +52,7 @@ defmodule Guardian.DBTest do end test "on_revoke with a record in the db", context do - Token.create! context.claims, "The JWT" + Token.create(context.claims, "The JWT") token = get_token() assert token != nil @@ -67,10 +64,10 @@ defmodule Guardian.DBTest do end test "purge stale tokens" do - Token.create! %{"jti" => "token1", "exp" => Guardian.timestamp + 5000}, "Token 1" - Token.create! %{"jti" => "token2", "exp" => Guardian.timestamp - 5000}, "Token 2" + Token.create(%{"jti" => "token1", "exp" => Guardian.timestamp() + 5000}, "Token 1") + Token.create(%{"jti" => "token2", "exp" => Guardian.timestamp() - 5000}, "Token 2") - Token.purge_expired_tokens! + Token.purge_expired_tokens() token1 = get_token("token1") token2 = get_token("token2") diff --git a/test/guardian/sweeper_test.exs b/test/guardian/sweeper_test.exs new file mode 100644 index 0000000..7d6461c --- /dev/null +++ b/test/guardian/sweeper_test.exs @@ -0,0 +1,24 @@ +defmodule Guardian.DB.Test.SweeperTest do + use Guardian.DB.Test.DataCase + + alias Guardian.DB.Token + alias Guardian.DB.Token.Sweeper + + test "purge stale tokens" do + Token.create(%{"jti" => "token1", "exp" => Guardian.timestamp() + 5000}, "Token 1") + Token.create(%{"jti" => "token2", "exp" => Guardian.timestamp() - 5000}, "Token 2") + + interval = 0 + state = %{interval: interval} + new_state = Sweeper.sweep(self(), state) + + token1 = get_token("token1") + token2 = get_token("token2") + + assert token1 != nil + assert token2 == nil + + assert new_state[:timer] != nil + assert_receive :sweep, interval + 10 + end +end diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 36e1c45..524134d 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -1,8 +1,9 @@ defmodule Guardian.DB.Test.DataCase do use ExUnit.CaseTemplate alias Guardian.DB.Test.Repo + alias Guardian.DB.Token - using(_opts) do + using _opts do quote do import Guardian.DB.Test.DataCase alias Guardian.DB.Test.Repo @@ -14,4 +15,7 @@ defmodule Guardian.DB.Test.DataCase do Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()}) :ok end + + def get_token(token_id \\ "token-uuid"), + do: Repo.get(Token.query_schema(), token_id) end