diff --git a/README.md b/README.md index 1ee1f8f..e94ef00 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -GuardianDb +Guardian.DB ========== -GuardianDb is an extension to vanilla Guardian that tracks tokens in your +Guardian.DB is an extension to vanilla Guardian that tracks tokens in your application's database to prevent playback. Support for `Guardian` 0.14.x is via the 0.8 release. @@ -9,15 +9,15 @@ Support for `Guardian` 0.14.x is via the 0.8 release. Installation ========== -GuardianDb assumes that you are using the Guardian framework for authentication. +Guardian.DB assumes that you are using the Guardian framework for authentication. -To install GuardianDb, first add it to your `mix.exs` file: +To install Guardian.DB, first add it to your `mix.exs` file: ```elixir defp deps do [ # ... - {:guardian_db, "~> 1.0.0"} + {:guardian_db, "~> 1.0"} # ... ] end @@ -34,19 +34,19 @@ run `mix guardian_db.gen.migration` to generate a migration. # Configuration ```elixir - config :guardian_db, GuardianDb, + config :guardian, Guardian.DB, repo: MyApp.Repo, schema_name: "guardian_tokens", # default sweep_interval: 60 # default: 60 minutes ``` -To sweep expired tokens from your db you should add `GuardianDb.ExpiredSweeper` to your supervision tree. +To sweep expired tokens from your db you should add `Guardian.DB.ExpiredSweeper` to your supervision tree. ```elixir - worker(GuardianDb.ExpiredSweeper, []) + worker(Guardian.DB.ExpiredSweeper, []) ``` -`GuardianDb` works by hooking into the lifecycle of your token module. +`Guardian.DB` works by hooking into the lifecycle of your token module. You'll need to add it to: @@ -63,19 +63,19 @@ defmodule MyApp.AuthTokens do # snip... def after_encode_and_sign(resource, claims, token, _options) do - with {:ok, _} <- GuardianDb.after_encode_and_sign(resource, claims["typ"], claims, token) do + with {:ok, _} <- Guardian.DB.after_encode_and_sign(resource, claims["typ"], claims, token) do {:ok, token} end end def on_verify(claims, token, _options) do - with {:ok, _} <- GuardianDb.on_verify(claims, token) do + with {:ok, _} <- Guardian.DB.on_verify(claims, token) do {:ok, claims} end end def on_revoke(claims, token, _options) do - with {:ok, _} <- GuardianDb.on_revoke(claims, token) do + with {:ok, _} <- Guardian.DB.on_revoke(claims, token) do {:ok, claims} end end @@ -87,16 +87,16 @@ Now run the migration and you'll be good to go. Considerations ========== -Vanilla Guardian is already a very robust JWT solution. However, if your application needs the ability to immediately revoke and invalidate tokens that have already been generated, you need something like GuardianDb to build upon Guardian. +Vanilla Guardian is already a very robust JWT solution. However, if your application needs the ability to immediately revoke and invalidate tokens that have already been generated, you need something like Guardian.DB to build upon Guardian. In vanilla Guardian, you as a systems administrator have no way of revoking tokens that have already been generated. You can call `Guardian.revoke!`, but in vanilla Guardian that function does not actually do anything - it just provides hooks for other libraries, such as this one, to define more specific behavior. Discarding the token after something like a log out action is left up to the client application. If the client application does not discard the token, or does not log out, or the token gets stolen by a malicious script (because the client application stores it in localStorage, for instance), the only thing you can do is wait until the token expires. Depending on the scenario, this may not be acceptable. -With GuardianDb, records of all generated tokens are kept in your application's database. During each request, the `Guardian.Plug.VerifyHeader` and `Guardian.Plug.VerifySession` plugs check the database to make sure the token is there. If it is not, the server returns a 401 Unauthorized response to the client. Furthermore, `Guardian.revoke!` behavior becomes enhanced, as it actually removes the token from the database. This means that if the user logs out, or you revoke their token (e.g. after noticing suspicious activity on the account), they will need to re-authenticate. +With Guardian.DB, records of all generated tokens are kept in your application's database. During each request, the `Guardian.Plug.VerifyHeader` and `Guardian.Plug.VerifySession` plugs check the database to make sure the token is there. If it is not, the server returns a 401 Unauthorized response to the client. Furthermore, `Guardian.revoke!` behavior becomes enhanced, as it actually removes the token from the database. This means that if the user logs out, or you revoke their token (e.g. after noticing suspicious activity on the account), they will need to re-authenticate. ### Disadvantages In vanilla Guardian, token verification is very light-weight. The only thing Guardian does is decode incoming tokens and make sure they are valid. This can make it much easier to horizontally scale your application, since there is no need to centrally store sessions and make them available to load balancers or other servers. -With GuardianDb, every request requires a trip to the database, as Guardian now needs to ensure that a record of the token exists. In large scale applications this can be fairly costly, and can arguably eliminate the main advantage of using a JWT authentication solution, which is statelessness. Furthermore, session authentication already works this way, and in most cases there isn't a good enough reason to reinvent that wheel using JWTs. +With Guardian.DB, every request requires a trip to the database, as Guardian now needs to ensure that a record of the token exists. In large scale applications this can be fairly costly, and can arguably eliminate the main advantage of using a JWT authentication solution, which is statelessness. Furthermore, session authentication already works this way, and in most cases there isn't a good enough reason to reinvent that wheel using JWTs. -In other words, once you have reached a point where you think you need GuardianDb, it may be time to take a step back and reconsider your whole approach to authentication! +In other words, once you have reached a point where you think you need Guardian.DB, it may be time to take a step back and reconsider your whole approach to authentication! diff --git a/config/config.exs b/config/config.exs index 535d44d..3072f4d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,16 +1,15 @@ use Mix.Config -config :guardian, Guardian, - issuer: "GuardianDb", - secret_key: "woeirulkjosiujgwpeiojlkjw3prowiuefoskjd", - serializer: GuardianDb.Test.Serializer +config :guardian, Guardian.DB, + issuer: "GuardianDB", + secret_key: "HcdlxxmyDRvfrwdpjUPh2M8mWP+KtpOQK1g6fT5SHrnflSY8KiWeORqN6IZSJYTA" -config :guardian_db, GuardianDb, - repo: GuardianDb.Test.Repo +config :guardian, Guardian.DB, + repo: Guardian.DB.Test.Repo -config :guardian_db, ecto_repos: [GuardianDb.Test.Repo] +config :guardian_db, ecto_repos: [Guardian.DB.Test.Repo] -config :guardian_db, GuardianDb.Test.Repo, +config :guardian_db, Guardian.DB.Test.Repo, adapter: Ecto.Adapters.Postgres, database: "guardian_db_test", pool: Ecto.Adapters.SQL.Sandbox, diff --git a/lib/guardian_db.ex b/lib/guardian/db.ex similarity index 75% rename from lib/guardian_db.ex rename to lib/guardian/db.ex index 6863b4b..d576077 100644 --- a/lib/guardian_db.ex +++ b/lib/guardian/db.ex @@ -1,11 +1,11 @@ -defmodule GuardianDb do +defmodule Guardian.DB do @moduledoc """ - GuardianDb is a simple module that hooks into guardian to prevent playback of tokens. + Guardian.DB is a simple module that hooks into guardian to prevent playback of tokens. In vanilla Guardian, tokens aren't tracked so the main mechanism that exists to make a token inactive is to set the expiry and wait until it arrives. - GuardianDb takes an active role and stores each token in the database verifying it's presense + Guardian.DB takes an active role and stores each token in the database verifying it's presense (based on it's jti) when Guardian verifies the token. If the token is not present in the DB, the Guardian token cannot be verified. @@ -31,17 +31,17 @@ defmodule GuardianDb do ### Sweeper - In order to sweep your expired tokens from the db, you'll need to add `GuardianDb.ExpiredSweeper` + In order to sweep your expired tokens from the db, you'll need to add `Guardian.DB.ExpiredSweeper` to your supervision tree. In your supervisor add it as a worker ```elixir - worker(GuardianDb.ExpiredSweeper, []) + worker(Guardian.DB.ExpiredSweeper, []) ``` # Migration - GuardianDb requires a table in your database. Create a migration like the following: + Guardian.DB requires a table in your database. Create a migration like the following: ```elixir create table(:guardian_tokens, primary_key: false) do @@ -60,7 +60,7 @@ defmodule GuardianDb do # Setup (Guardian >= 1.0) - GuardianDb works by hooking into the lifecycle of your token module. + Guardian.DB works by hooking into the lifecycle of your token module. You'll need to add it to @@ -77,19 +77,19 @@ defmodule GuardianDb do # snip... def after_encode_and_sign(resource, claims, token, _options) do - with {:ok, _} <- GuardianDb.after_encode_and_sign(resource, claims["typ"], claims, token) do + with {:ok, _} <- Guardian.DB.after_encode_and_sign(resource, claims["typ"], claims, token) do {:ok, token} end end def on_verify(claims, token, _options) do - with {:ok, _} <- GuardianDb.on_verify(claims, token) do + with {:ok, _} <- Guardian.DB.on_verify(claims, token) do {:ok, claims} end end def on_revoke(claims, token, _options) do - with {:ok, _} <- GuardianDb.on_revoke(claims, token) do + with {:ok, _} <- Guardian.DB.on_revoke(claims, token) do {:ok, claims} end end @@ -98,16 +98,22 @@ defmodule GuardianDb do # Setup (Guardian < 1.0) - To use `GuardianDb` with Guardian less than version 1.0, add GuardianDb as your + To use `Guardian.DB` with Guardian less than version 1.0, add Guardian.DB as your hooks module. In the Guardian configuration: ```elixir config :guardian, Guardian, - hooks: GuardianDb + hooks: Guardian.DB ``` """ - alias GuardianDb.Token + alias Guardian.DB.Token + + config = Application.get_env(:guardian, Guardian.DB, []) + @repo Keyword.get(config, :repo) + + if config == [], do: raise("Guardian.DB configuration is required") + if is_nil(@repo), do: raise("Guardian.DB requires a repo") @doc """ After the JWT is generated, stores the various fields of it in the DB for tracking @@ -150,8 +156,8 @@ defmodule GuardianDb do end def repo do - :guardian_db - |> Application.fetch_env!(GuardianDb) + :guardian + |> Application.fetch_env!(Guardian.DB) |> Keyword.fetch!(:repo) end end diff --git a/lib/guardian_db/expired_sweeper.ex b/lib/guardian/db/expired_sweeper.ex similarity index 88% rename from lib/guardian_db/expired_sweeper.ex rename to lib/guardian/db/expired_sweeper.ex index 42656b3..01ef7a0 100644 --- a/lib/guardian_db/expired_sweeper.ex +++ b/lib/guardian/db/expired_sweeper.ex @@ -1,17 +1,17 @@ -defmodule GuardianDb.ExpiredSweeper do +defmodule Guardian.DB.ExpiredSweeper do @moduledoc """ Periocially purges expired tokens from the DB. ## Example - config :guardian_db, GuardianDb, + config :guardian, Guardian.DB, sweep_interval: 60 # 1 hour # in your supervisor - worker(GuardianDb.ExpiredSweeper, []) + worker(Guardian.DB.ExpiredSweeper, []) """ use GenServer - alias GuardianDb.Token + alias Guardian.DB.Token def start_link, do: start_link([]) @@ -71,8 +71,8 @@ defmodule GuardianDb.ExpiredSweeper do end defp interval do - :guardian_db - |> Application.get_env(GuardianDb) + :guardian + |> Application.get_env(Guardian.DB) |> Keyword.get(:sweep_interval, 60) |> minute_to_ms end diff --git a/lib/guardian_db/token.ex b/lib/guardian/db/token.ex similarity index 84% rename from lib/guardian_db/token.ex rename to lib/guardian/db/token.ex index 239ad26..1c32691 100644 --- a/lib/guardian_db/token.ex +++ b/lib/guardian/db/token.ex @@ -1,4 +1,4 @@ -defmodule GuardianDb.Token do +defmodule Guardian.DB.Token do @moduledoc """ A very simple model for storing tokens generated by guardian. """ @@ -7,7 +7,7 @@ defmodule GuardianDb.Token do import Ecto.Changeset import Ecto.Query, only: [where: 3] - alias GuardianDb.Token + alias Guardian.DB.Token @primary_key {:jti, :string, autogenerate: false} @allowed_fields ~w(jti typ aud iss sub exp jwt claims)a @@ -31,7 +31,7 @@ defmodule GuardianDb.Token do jti = Map.get(claims, "jti") aud = Map.get(claims, "aud") - GuardianDb.repo().get_by(query_schema(), jti: jti, aud: aud) + Guardian.DB.repo().get_by(query_schema(), jti: jti, aud: aud) end @doc """ @@ -46,7 +46,7 @@ defmodule GuardianDb.Token do %Token{} |> Ecto.put_meta(source: schema_name()) |> cast(prepared_claims, @allowed_fields) - |> GuardianDb.repo().insert() + |> Guardian.DB.repo().insert() end @doc """ @@ -57,7 +57,7 @@ defmodule GuardianDb.Token do query_schema() |> where([token], token.exp < ^timestamp) - |> GuardianDb.repo.delete_all() + |> Guardian.DB.repo().delete_all() end def query_schema do @@ -65,8 +65,8 @@ defmodule GuardianDb.Token do end def schema_name do - :guardian_db - |> Application.fetch_env!(GuardianDb) + :guardian + |> Application.fetch_env!(Guardian.DB) |> Keyword.get(:schema_name, "guardian_tokens") end end diff --git a/lib/mix/tasks/guardian_db.gen.migration.ex b/lib/mix/tasks/guardian_db.gen.migration.ex index 321a872..f0d3ff8 100644 --- a/lib/mix/tasks/guardian_db.gen.migration.ex +++ b/lib/mix/tasks/guardian_db.gen.migration.ex @@ -1,5 +1,5 @@ -defmodule Mix.Tasks.GuardianDb.Gen.Migration do - @shortdoc "Generates GuardianDb's migration" +defmodule Mix.Tasks.Guardian.DB.Gen.Migration do + @shortdoc "Generates Guardian.DB's migration" @moduledoc """ Generates the required GuardianDb's database migration diff --git a/mix.exs b/mix.exs index cbc5df5..da8f804 100644 --- a/mix.exs +++ b/mix.exs @@ -1,4 +1,4 @@ -defmodule GuardianDb.Mixfile do +defmodule Guardian.DB.Mixfile do use Mix.Project @version "1.0.0" diff --git a/priv/templates/migration.exs.eex b/priv/templates/migration.exs.eex index 81cd0ed..f5b2d64 100644 --- a/priv/templates/migration.exs.eex +++ b/priv/templates/migration.exs.eex @@ -1,4 +1,4 @@ -defmodule <%= module_prefix %>.Repo.Migrations.GuardianDb do +defmodule <%= module_prefix %>.Repo.Migrations.Guardian.DB do use Ecto.Migration def change do diff --git a/priv/test/migrations/20160929125415_migrations.exs b/priv/test/migrations/20160929125415_migrations.exs index a4902f9..e4407ef 100644 --- a/priv/test/migrations/20160929125415_migrations.exs +++ b/priv/test/migrations/20160929125415_migrations.exs @@ -1,4 +1,4 @@ -defmodule GuardianDb.Test.Repo.Migrations do +defmodule Guardian.DB.Test.Repo.Migrations do use Ecto.Migration def up do diff --git a/test/guardian_db_test.exs b/test/guardian/db_test.exs similarity index 75% rename from test/guardian_db_test.exs rename to test/guardian/db_test.exs index d4738ec..d4e04ab 100644 --- a/test/guardian_db_test.exs +++ b/test/guardian/db_test.exs @@ -1,6 +1,6 @@ -defmodule GuardianDbTest do - use GuardianDb.Test.DataCase - alias GuardianDb.Token +defmodule Guardian.DBTest 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) @@ -21,7 +21,7 @@ defmodule GuardianDbTest do token = get_token() assert token == nil - GuardianDb.after_encode_and_sign(%{}, :token, context.claims, "The JWT") + Guardian.DB.after_encode_and_sign(%{}, :token, context.claims, "The JWT") token = get_token() @@ -39,19 +39,19 @@ defmodule GuardianDbTest do token = get_token() assert token != nil - assert {:ok, {context.claims, "The JWT"}} == GuardianDb.on_verify(context.claims, "The JWT") + assert {:ok, {context.claims, "The JWT"}} == Guardian.DB.on_verify(context.claims, "The JWT") end test "on_verify without a record in the db", context do token = get_token() assert token == nil - assert {:error, :token_not_found} == GuardianDb.on_verify(context.claims, "The JWT") + assert {:error, :token_not_found} == Guardian.DB.on_verify(context.claims, "The JWT") end test "on_revoke without a record in the db", context do token = get_token() assert token == nil - assert GuardianDb.on_revoke(context.claims, "The JWT") == {:ok, {context.claims, "The JWT"}} + assert Guardian.DB.on_revoke(context.claims, "The JWT") == {:ok, {context.claims, "The JWT"}} end test "on_revoke with a record in the db", context do @@ -60,7 +60,7 @@ defmodule GuardianDbTest do token = get_token() assert token != nil - assert GuardianDb.on_revoke(context.claims, "The JWT") == {:ok, {context.claims, "The JWT"}} + assert Guardian.DB.on_revoke(context.claims, "The JWT") == {:ok, {context.claims, "The JWT"}} token = get_token() assert token == nil diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 768a958..36e1c45 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -1,11 +1,11 @@ -defmodule GuardianDb.Test.DataCase do +defmodule Guardian.DB.Test.DataCase do use ExUnit.CaseTemplate - alias GuardianDb.Test.Repo + alias Guardian.DB.Test.Repo using(_opts) do quote do - import GuardianDb.Test.DataCase - alias GuardianDb.Test.Repo + import Guardian.DB.Test.DataCase + alias Guardian.DB.Test.Repo end end diff --git a/test/support/repo.ex b/test/support/repo.ex index 084c72c..71ac7eb 100644 --- a/test/support/repo.ex +++ b/test/support/repo.ex @@ -1,4 +1,4 @@ -defmodule GuardianDb.Test.Repo do +defmodule Guardian.DB.Test.Repo do use Ecto.Repo, otp_app: :guardian_db def log(_cmd), do: nil diff --git a/test/support/serializer.ex b/test/support/serializer.ex index 090e182..364b9fd 100644 --- a/test/support/serializer.ex +++ b/test/support/serializer.ex @@ -1,6 +1,11 @@ -defmodule GuardianDb.Test.Serializer do - @behaviour Guardian.Serializer +defmodule Guardian.DB.Test.Serializer do + use Guardian, otp_app: :guardian_db - def for_token(sub), do: {:ok, sub} - def from_token(sub), do: {:ok, sub} + def subject_for_token(resource, _claims) do + {:ok, resource} + end + + def resource_from_claims(claims) do + {:ok, claims["sub"]} + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index a702b4c..2de5a26 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1,2 @@ -{:ok, _pid} = GuardianDb.Test.Repo.start_link() +{:ok, _pid} = Guardian.DB.Test.Repo.start_link() ExUnit.start()