Skip to content

Commit

Permalink
Merge pull request #6 from mdlkxzmcp/chore/bump-guardian-db
Browse files Browse the repository at this point in the history
Bump GuardianDB from 2.1.1 to 3.0.0
  • Loading branch information
alexfilatov authored Feb 23, 2024
2 parents c61776b + 992226d commit 996555b
Show file tree
Hide file tree
Showing 9 changed files with 341 additions and 317 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## Unreleased
GuardianDB 3.0 changes the way the it's configuration is done.
Change
```elixir
config :guardian, Guardian.DB, repo: GuardianRedis.Repo
```
in your config to:
```elixir
config :guardian, Guardian.DB, adapter: GuardianRedis.Adapter
```
### Breaking
- bump `GuardianDB` from 2.1.1 to 3.0.0

## V0.1.0

Initial release
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
[![Hex.pm](https://img.shields.io/hexpm/v/guardian_redis.svg)](https://hex.pm/packages/guardian_redis)
![Build Status](https://github.com/alexfilatov/guardian_redis/workflows/Continuous%20Integration/badge.svg)

Redis repository for Guardian DB.
Redis adapter for Guardian DB.

## Installation

You can use `GuardianRedis` in case you use [Guardian](https://github.com/ueberauth/guardian) library for authentication
and [Guardian.DB](https://github.com/ueberauth/guardian_db) library for JWT tokens persistence.
You can use `GuardianRedis` in case you use [Guardian](https://github.com/ueberauth/guardian) library for authentication
and [Guardian.DB](https://github.com/ueberauth/guardian_db) library for JWT tokens persistence.

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `guardian_redis` to your list of dependencies in `mix.exs`:
Expand All @@ -24,12 +24,12 @@ end

## Configuration

All you need to install Guardian DB as per [Guardian.DB README](https://github.com/ueberauth/guardian_db#readme)
and just use `GuardianRedis.Repo` as a `repo` in settings.
All you need to install Guardian DB as per [Guardian.DB README](https://github.com/ueberauth/guardian_db#readme)
and just use `GuardianRedis.Adapter` as a `adapter` in settings.

```elixir
config :guardian, Guardian.DB,
repo: GuardianRedis.Repo # Add this Redis repository module
adapter: GuardianRedis.Adapter # Add this Redis adapter module
```

Add GuardianRedis.Redix to your supervision tree:
Expand Down Expand Up @@ -60,11 +60,11 @@ config :guardian_redis, :redis,
```


## Implement Guardian.DB repo for a different storage
## Implement Guardian.DB adapter for a different storage

Initially, Guardian.DB was aimed to store and operate JWT tokens in a PostgreSQL database.
Sometimes round trip to Postgres database is expensive so this is why this Redis repo was born.
In case you want to implement a possibility for Guardian.DB to use different storage, e.g. ETS (or MySQL),
Initially, Guardian.DB was aimed to store and operate JWT tokens in a PostgreSQL database.
Sometimes round trip to Postgres database is expensive so this is why this Redis adapter was born.
In case you want to implement a possibility for Guardian.DB to use different storage, e.g. ETS (or MySQL),
you need to implement `Guardian.DB.Adapter` behavior. Thanks to [@aleDsz](https://github.com/aleDsz) it's quite simple:
https://github.com/ueberauth/guardian_db/blob/master/lib/guardian/db/adapter.ex

Expand Down
9 changes: 1 addition & 8 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
use Mix.Config

config :guardian, Guardian.DB,
# Add your repository module
repo: GuardianRedis.Repo,
# default: 60 minutes
sweep_interval: 60

# store all token types if not set
# token_types: ["refresh_token"],
config :guardian, Guardian.DB, adapter: GuardianRedis.Adapter

config :guardian_redis, :redis,
host: System.get_env("REDIS_HOST", "127.0.0.1"),
Expand Down
134 changes: 110 additions & 24 deletions lib/adapter.ex
Original file line number Diff line number Diff line change
@@ -1,41 +1,127 @@
defmodule Guardian.DB.Adapter do
defmodule GuardianRedis.Adapter do
@moduledoc """
The Guardian DB Adapter.
An adapter for `Guardian.DB` that uses Redis.
This behaviour allows to use any storage system
for Guardian Tokens.
Serves only `Guardian.DB` purpose, do not use it as a Redis adapter for your project.
Dependant on `Jason` and `Redix`.
Stores and deletes JWT token to/from Redis using key combined from JWT.jti and JWT.aud.
Module stores JWT token in Redis using automatic expiry feature of Redis so we don't need to run token sweeper.
"""
@behaviour Guardian.DB.Adapter

@typep query :: Ecto.Query.t()
@typep schema :: Ecto.Schema.t()
@typep changeset :: Ecto.Changeset.t()
@typep schema_or_changeset :: schema() | changeset()
@typep queryable :: query() | schema()
@typep opts :: keyword()
@typep id :: pos_integer() | binary() | Ecto.UUID.t()
alias Guardian.DB.Token
alias GuardianRedis.Redix, as: Redis

@doc """
Retrieves JWT token
Used in `Guardian.DB.Token.find_by_claims/1`
Fetches a single result from the query.
Returns nil if no result was found. Raises if more than one entry.
"""
@impl true
def one(query, _opts) do
key = key(query)

case Redis.command(["GET", key]) do
{:ok, nil} -> nil
{:ok, jwt_json} -> Jason.decode!(jwt_json)
_ -> nil
end
end

@doc """
Insert Token into Redis.
Token is auto expired in `expired_in` seconds based on JWT `exp` value.
"""
@callback one(queryable()) :: schema() | nil
@impl true
def insert(struct, _opts) do
key = key(struct)

expires_in = struct.changes.exp - System.system_time(:second)
utc_now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)

token =
struct.changes
|> Map.put(:inserted_at, utc_now)
|> Map.put(:updated_at, utc_now)

case Redis.command(["SETEX", key, Integer.to_string(expires_in), Jason.encode!(token)]) do
{:ok, "OK"} ->
# Adding key to the set `sub` (user_id in JWT) so we can delete all user tokens in one go
sub = sub_elem(struct)
Redis.command(["SADD", set_name(sub), key])
{:ok, struct(Token, token)}

error ->
{:error, error}
end
end

@doc """
Persists JWT token
Used in `Guardian.DB.Token.create/2`
Remove a user token, log out functionality, invalidation of a JWT token.
"""
@callback insert(schema_or_changeset()) :: {:ok, schema()} | {:error, changeset()}
@impl true
def delete(model, _opts) do
key = key(model)

case Redis.command(["DEL", key]) do
{:ok, _num} -> {:ok, struct(Token, model)}
_ -> {:error, model}
end
end

@doc """
Deletes JWT token
Used in `Guardian.DB.Token.destroy_token/3`
Remove all tokens that belong to given `sub`.
"""
@callback delete(schema_or_changeset(), opts()) :: {:ok, schema()} | {:error, changeset()}
@impl true
def delete_by_sub(sub, _opts) do
set_name = set_name(sub)

{:ok, keys} = Redis.command(["SMEMBERS", set_name])
{:ok, amount_deleted} = Redis.command(["DEL", set_name] ++ keys)

if amount_deleted > 0,
do: {amount_deleted - 1, nil},
else: {0, nil}
end

@doc """
Purges all JWT tokens
Used in `Guardian.DB.Token.purge_expired_tokens/0 and in `Guardian.DB.Token.destroy_by_sub/1`
Returns a tuple containing the number of entries and any returned result as second element.
Mock implementation as Redis has built in key expiration that is in use.
"""
@callback delete_all(queryable(), opts()) :: {integer(), nil | [term()]}
@impl true
def purge_expired_tokens(_timestamp, _opts), do: {0, nil}

# Generate Redis key from a changeset, %Token{} struct or Ecto.Query
defp key(%{changes: %{jti: jti, aud: aud}}), do: combine_key(jti, aud)
defp key(%{"jti" => jti, "aud" => aud}), do: combine_key(jti, aud)
defp key(query), do: combine_key(jti_elem(query), aud_elem(query))
defp sub_elem(%{changes: %{sub: sub}}), do: sub
defp sub_elem(%{"sub" => sub}), do: sub
defp sub_elem(query), do: query_param(query, :sub)
defp jti_elem(query), do: query_param(query, :jti)
defp aud_elem(query), do: query_param(query, :aud)
defp combine_key(jti, aud), do: "#{jti}:#{aud}"
defp set_name(sub), do: "set:#{sub}"

# Retrieves params from `query.wheres` by atom name (`:jti` and `:aud` in our case), example:
# ```
# [
# %Ecto.Query.BooleanExpr{
# ...
# params: [
# {"2e024736-b4a6-4422-8b0a-4e89c7a7ebf9", {0, :jti}},
# {"my_app", {0, :aud}}
# ],
# ...
# }
# ]
# ```
defp query_param(query, param) do
(query.wheres |> List.first()).params
|> Enum.find(fn i -> i |> elem(1) |> elem(1) == param end)
|> elem(0)
end
end
132 changes: 0 additions & 132 deletions lib/repo.ex

This file was deleted.

4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule GuardianRedis.MixProject do
name: "GuardianRedis",
app: :guardian_redis,
version: @version,
description: "Redis repo for Guardian DB",
description: "Redis adapter for Guardian DB",
elixir: "~> 1.4 or ~> 1.5",
elixirc_paths: elixirc_paths(Mix.env()),
package: package(),
Expand All @@ -36,7 +36,7 @@ defmodule GuardianRedis.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:guardian_db, "~> 2.0"},
{:guardian_db, "~> 3.0"},
{:redix, "~> 1.1"},
{:jason, "~> 1.1"},

Expand Down
Loading

0 comments on commit 996555b

Please sign in to comment.