Skip to content

Commit

Permalink
Merge pull request #131 from ueberauth/ets-adapter
Browse files Browse the repository at this point in the history
Implement an ETS storage adapter
  • Loading branch information
yordis committed Sep 11, 2023
2 parents 31d13d1 + 19637e0 commit 38dd6ec
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 3 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
## v3.0.0

* Introduced `Guardian.DB.Adapter` behaviour to allow for custom database adapters to be used with Guardian DB.
Add`config :guardian, Guardian.DB, adapter: Guardian.DB.EctoAdapter` to fallback to the default Ecto adapter.
- Add `config :guardian, Guardian.DB, adapter: Guardian.DB.EctoAdapter` to fall back to the default Ecto adapter.
- Added `Guardian.DB.ETSAdapter`.
- Added `Guardian.DB.EctoAdapter`.
* Allow migrations mix task with custom table name.
* Make `jti` and `aud` required fields, since they are primary keys.

Expand Down
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use Mix.Config
import Config

config :guardian, Guardian.DB,
issuer: "GuardianDB",
Expand Down
90 changes: 90 additions & 0 deletions lib/guardian/db/ets_adapter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
defmodule Guardian.DB.ETSAdapter do
@moduledoc """
Implement the Guardian.DB.Adapter for ETS
"""

@behaviour Guardian.DB.Adapter

@impl true
def one(claims, opts) do
jti = Map.get(claims, "jti")
aud = Map.get(claims, "aud")

match =
opts
|> Keyword.fetch!(:table)
|> :ets.match({jti, aud, :_, :_, :"$1"})

case match do
[[token]] -> token
_ -> nil
end
end

@impl true
def insert(%{valid?: true} = changeset, opts) do
table = Keyword.fetch!(opts, :table)
token = Map.merge(changeset.data, changeset.changes)

unless :ets.insert(table, {token.jti, token.aud, token.sub, token.exp, token}) do
raise """
An error occurred trying to insert a new record into the ETS table #{table}.
Please ensure you've created the table before attempting to insert records.
"""
end

{:ok, token}
end

def insert(changeset, _opts) do
{:error, changeset}
end

@impl true
def delete(%{jti: jti} = token, opts) do
table = Keyword.fetch!(opts, :table)

unless :ets.delete(table, jti) do
raise """
An error occurred trying to delete a record from the ETS table #{table}.
Please ensure you've created the table before attempting to delete records.
"""
end
end

@impl true
def delete_by_sub(sub, opts) do
table = Keyword.fetch!(opts, :table)

table
|> :ets.match({:"$1", :_, sub, :_, :"$2"})
|> delete_many(table)
end

@impl true
def purge_expired_tokens(exp, opts) do
table = Keyword.fetch!(opts, :table)
matcher = [{{:"$1", :"$2", :"$3", :"$4", :"$5"}, [{:<, :"$4", exp}], [[:"$1", :"$5"]]}]

table
|> :ets.select(matcher)
|> delete_many(table)
end

defp expired_tokens({_jti, _aud, _sub, token}, exp), do: token.exp < exp

defp delete_many(tokens, table) do
deleted_tokens =
Enum.reduce(tokens, [], fn [jti, token], acc ->
if :ets.delete(table, jti) do
[token | acc]
else
acc
end
end)

{length(deleted_tokens), deleted_tokens}
end
end
6 changes: 5 additions & 1 deletion lib/guardian/db/token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ defmodule Guardian.DB.Token do
Create a new token based on the JWT and decoded claims.
"""
def create(claims, jwt) do
adapter().insert(changeset(claims, jwt), config())
end

@doc false
def changeset(claims, jwt) do
prepared_claims =
claims
|> Map.put("jwt", jwt)
Expand All @@ -44,7 +49,6 @@ defmodule Guardian.DB.Token do
%Token{}
|> cast(prepared_claims, @allowed_fields)
|> validate_required(@required_fields)
|> adapter().insert(config())
end

@doc """
Expand Down
110 changes: 110 additions & 0 deletions test/guardian/adapter/ets_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
defmodule Guardian.DB.ETSAdapterTest do
use ExUnit.Case

alias Guardian.DB.ETSAdapter, as: Adapter
alias Guardian.DB.Token

setup_all do
{:ok, table: :ets.new(:guardian_db_test, [:set, :public])}
end

describe "insert/2" do
test "creates a new row and returns the token", %{table: table} do
claims = %{
"jti" => "token-jti-insert-test",
"typ" => "token-typ",
"aud" => "token-aud",
"sub" => "token-aub",
"iss" => "token-iss",
"exp" => Guardian.timestamp() + 1_000_000_000
}

assert {:ok, %{jti: "token-jti-insert-test", jwt: "test-jwt"}} =
claims
|> Token.changeset("test-jwt")
|> Adapter.insert(table: table)
end
end

describe "one/2" do
test "returns the token by claims", %{table: table} do
token = %Token{
aud: "token-aud",
exp: Guardian.timestamp() + 1_000_000_000,
jti: "token-jti-one-test",
sub: "token-sub"
}

:ets.insert(table, {token.jti, token.aud, token.sub, token.exp, token})

assert %Token{} =
Adapter.one(%{"aud" => "token-aud", "jti" => "token-jti-one-test"}, table: table)
end
end

describe "delete/2" do
test "deletes and returns the token", %{table: table} do
token = %Token{
aud: "token-aud",
exp: Guardian.timestamp() + 1_000_000_000,
jti: "token-jti-delete-test",
sub: "token-sub"
}

:ets.insert(table, {token.jti, token.aud, token.sub, token.exp, token})

assert {:ok, %Token{}} = Adapter.delete(token, table: table)

assert [] = :ets.match(table, {token.jti, :_, :_, :"$1"})
end
end

describe "delete_by_sub/2" do
test "deletes many tokens by the subject", %{table: table} do
one = %Token{
aud: "token-aud",
exp: Guardian.timestamp() + 1_000_000_000,
jti: "token-jti-delete-by-sub-test1",
sub: "subject"
}

two = %Token{
aud: "token-aud",
exp: Guardian.timestamp() + 1_000_000_000,
jti: "token-jti-delete-by-sub-test2",
sub: "subject"
}

:ets.insert(table, {one.jti, one.aud, one.sub, one.exp, one})
:ets.insert(table, {two.jti, two.aud, two.sub, two.exp, two})

assert {2, [%Token{}, %Token{}]} = Adapter.delete_by_sub("subject", table: table)

assert [] = :ets.match(table, {:_, :_, "subject", :"$1"})
end
end

describe "purge_expired_tokens/2" do
test "deletes all tokens older than expiration", %{table: table} do
one = %Token{
aud: "token-aud",
exp: 1,
jti: "token-jti-purge-test1",
sub: "token-sub"
}

two = %Token{
aud: "token-aud",
exp: Guardian.timestamp() + 1_000_000_000,
jti: "token-jti-purge-test2",
sub: "token-sub"
}

:ets.insert(table, {one.jti, one.aud, one.sub, one.exp, one})
:ets.insert(table, {two.jti, two.aud, two.sub, two.exp, two})

assert {1, [%Token{jti: "token-jti-purge-test1"}]} =
Adapter.purge_expired_tokens(Guardian.timestamp(), table: table)
end
end
end

0 comments on commit 38dd6ec

Please sign in to comment.