Skip to content

Commit

Permalink
Move GuardianDb into the Guardian.* namespace (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
doomspork authored Nov 28, 2017
1 parent ccaf24c commit 99a5cbc
Show file tree
Hide file tree
Showing 14 changed files with 85 additions and 75 deletions.
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
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.

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
Expand All @@ -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:

Expand All @@ -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
Expand All @@ -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!
15 changes: 7 additions & 8 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
36 changes: 21 additions & 15 deletions lib/guardian_db.ex → lib/guardian/db.ex
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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([])

Expand Down Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions lib/guardian_db/token.ex → lib/guardian/db/token.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule GuardianDb.Token do
defmodule Guardian.DB.Token do
@moduledoc """
A very simple model for storing tokens generated by guardian.
"""
Expand All @@ -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
Expand All @@ -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 """
Expand All @@ -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 """
Expand All @@ -57,16 +57,16 @@ 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
{schema_name(), Token}
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
4 changes: 2 additions & 2 deletions lib/mix/tasks/guardian_db.gen.migration.ex
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule GuardianDb.Mixfile do
defmodule Guardian.DB.Mixfile do
use Mix.Project

@version "1.0.0"
Expand Down
2 changes: 1 addition & 1 deletion priv/templates/migration.exs.eex
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion priv/test/migrations/20160929125415_migrations.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule GuardianDb.Test.Repo.Migrations do
defmodule Guardian.DB.Test.Repo.Migrations do
use Ecto.Migration

def up do
Expand Down
16 changes: 8 additions & 8 deletions test/guardian_db_test.exs → test/guardian/db_test.exs
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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()

Expand All @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 99a5cbc

Please sign in to comment.