Skip to content

Commit

Permalink
[PLATFORM-2332]: Implement caching using dynamo (#251)
Browse files Browse the repository at this point in the history
  • Loading branch information
MaeIsBad authored Dec 5, 2024
1 parent a4046f9 commit b294331
Show file tree
Hide file tree
Showing 16 changed files with 345 additions and 98 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
echo "
127.0.0.1 localauth0
127.0.0.1 redis
127.0.0.1 aws
" | sudo tee /etc/hosts
Expand Down Expand Up @@ -106,6 +107,12 @@ jobs:
volumes:
- ./:/repo:ro
options: --name localauth0
aws:
image: public.ecr.aws/localstack/localstack:4
ports:
- "4566:4566"
env:
ALLOW_NONSTANDARD_REGIONS: 1

alls-green:
if: always()
Expand Down
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [0.9.0] - 2024-12-04

# Added

A new DynamoDB cache provider

---

## [0.8.0] - 2024-11-29

### Changed
Expand Down Expand Up @@ -305,7 +313,9 @@ Bug fixes

- Fixed compilation error when `:auth0_ex, :server` is not configured in `config.exs`

[Unreleased]: https://github.com/primait/auth0_ex/compare/0.8.0...HEAD

[Unreleased]: https://github.com/primait/auth0_ex/compare/0.9.0...HEAD
[0.9.0]: https://github.com/primait/auth0_ex/compare/0.8.0...0.9.0
[0.8.0]: https://github.com/primait/auth0_ex/compare/0.7.1...0.8.0
[0.7.1]: https://github.com/primait/auth0_ex/compare/0.7.0...0.7.1
[0.7.0]: https://github.com/primait/auth0_ex/compare/0.7.0-pre.0...0.7.0
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,27 @@ applying the `PrimaAuth0Ex.TokenCache` behavior. This involves substituting the
`config :prima_auth0_ex, :token_cache, EncryptedRedisTokenCache` configuration with the newly crafted custom TokenCache
implementation.

### DynamoDB

A new, dynamodb base caching mechanism is available. To use it you will need to configure `ex_aws` credentials, and set a table name for auth0_ex to use. For example:

```
config :prima_auth0_ex,
token_cache: DynamoDB,
# See ex_aws docs
config :ex_aws,
access_key_id: "key-id",
secret_access_key: "secret"
config :ex_aws, :dynamodb,
region: "eu-west-1"
config :prima_auth0_ex, :dynamodb, table_name: "prima_auth0_ex_token_cache"
```

Make sure auth0_ex has full permissions to create, read, write and update the table.

#### Operational requirements

To cache tokens on Redis you'll need to generate a `cache_encryption_key`. This can be done either by running `mix keygen` or by using the following snippet:
Expand Down
10 changes: 10 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@ import Config
config :prima_auth0_ex, :redis, enabled: false

config :logger, level: :debug

config :ex_aws,
access_key_id: "ABCD",
secret_access_key: "secret"

config :ex_aws, :dynamodb,
scheme: "http://",
host: "dynamodb",
port: 4566,
region: "us-east-1"
12 changes: 12 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ config :prima_auth0_ex, :server,
issuer: "https://your-auth0-tenant.com",
first_jwks_fetch_sync: true

config :prima_auth0_ex, :dynamodb, table_name: "prima_auth0_ex_test_table"

config :prima_auth0_ex, :redis,
encryption_key: "uhOrqKvUi9gHnmwr60P2E1hiCSD2dtXK1i6dqkU4RTA=",
connection_uri: "redis://redis:6379",
Expand All @@ -40,4 +42,14 @@ config :prima_auth0_ex, :clients,
token_check_interval: :timer.seconds(1)
]

config :ex_aws,
access_key_id: "ABCD",
secret_access_key: "secret"

config :ex_aws, :dynamodb,
scheme: "http://",
host: "aws",
port: 4566,
region: "us-east-1"

config :logger, level: :warning
12 changes: 10 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,32 @@ services:
stdin_open: true
depends_on:
- redis
- aws
- localauth0

redis:
image: public.ecr.aws/bitnami/redis:5.0
ports:
- "6379:6379"
hostname: 'redis'
hostname: "redis"
environment:
- ALLOW_EMPTY_PASSWORD=yes

localauth0:
image: public.ecr.aws/c6i9l4r6/localauth0:0.6.2
ports:
ports:
- 3000:3000
environment:
LOCALAUTH0_CONFIG_PATH: /localauth0.toml
volumes:
- ./localauth0.toml:/localauth0.toml:ro

aws:
image: public.ecr.aws/localstack/localstack:4
ports:
- "4566:4566"
environment:
ALLOW_NONSTANDARD_REGIONS: 1

volumes:
app:
3 changes: 3 additions & 0 deletions lib/prima_auth0_ex/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ defmodule PrimaAuth0Ex.Config do
def redis(prop, default \\ nil), do: get_env(:redis, prop, default)
def redis!(prop), do: fetch_env!(:redis, prop)

def dynamodb(prop, default \\ nil), do: get_env(:dynamodb, prop, default)
def dynamodb!(prop), do: fetch_env!(:dynamodb, prop)

def refresh_strategy(default),
do: get_env(:refresh_strategy, default)

Expand Down
136 changes: 136 additions & 0 deletions lib/prima_auth0_ex/token_cache/dynamodb.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
defmodule PrimaAuth0Ex.TokenCache.DynamoDB.StoredToken do
@moduledoc false

alias PrimaAuth0Ex.TokenProvider.TokenInfo
@derive [ExAws.Dynamo.Encodable]
defstruct [:key, :jwt, :issued_at, :expires_at, :kid]

@type t :: %__MODULE__{
key: String.t(),
jwt: String.t(),
issued_at: non_neg_integer(),
expires_at: non_neg_integer(),
kid: String.t()
}

def from_token_info(key, %TokenInfo{expires_at: expires_at, issued_at: issued_at, kid: kid, jwt: jwt}) do
%__MODULE__{
key: key,
expires_at: expires_at,
issued_at: issued_at,
kid: kid,
jwt: jwt
}
end

def to_token_info(%__MODULE__{issued_at: issued_at, expires_at: expires_at, jwt: jwt, kid: kid}) do
%TokenInfo{
jwt: jwt,
kid: kid,
expires_at: expires_at,
issued_at: issued_at
}
end
end

defmodule PrimaAuth0Ex.TokenCache.DynamoDB do
@moduledoc """
Implementation of `PrimaAuth0Ex.TokenCache` that persists tokens on aws dynamodb
"""

alias ExAws.Dynamo

alias PrimaAuth0Ex.Config
alias PrimaAuth0Ex.TokenCache
alias PrimaAuth0Ex.TokenCache.DynamoDB.StoredToken
alias PrimaAuth0Ex.TokenProvider.TokenInfo

@behaviour TokenCache

@impl TokenCache
def child_spec(_) do
%{
id: __MODULE__,
start: {__MODULE__, :start, []},
restart: :transient
}
end

def start do
if create_table?() do
create_update_table()
end

:ignore
end

@impl TokenCache
# Dialyzer complains about the %{:ok, %{}} pattern never matching
# This is incorrect, most likely an issue with ExAws types.
# We do have a unit case that covers this
@dialyzer {:nowarn_function, get_token_for: 2}
def get_token_for(client \\ :default_client, audience) do
with request <- Dynamo.get_item(table_name(), %{key: key(client, audience)}, consistent_read: false),
{:ok, res} when res != %{} <- ExAws.request(request),
%StoredToken{} = stored_token <-
Dynamo.decode_item(res, as: StoredToken) do
{:ok, StoredToken.to_token_info(stored_token)}
else
{:ok, %{}} -> {:ok, nil}
{:error, error} -> {:error, error}
end
end

@impl TokenCache
def set_token_for(
client \\ :default_client,
audience,
%TokenInfo{} = token_info
) do
stored_token = StoredToken.from_token_info(key(client, audience), token_info)

case table_name() |> Dynamo.put_item(stored_token) |> ExAws.request() do
{:ok, _} -> :ok
{:error, err} -> {:error, err}
end
end

# More ExAws typing issues
@dialyzer {:nowarn_function, create_update_table: 0}
def create_update_table do
case table_name() |> Dynamo.describe_table() |> ExAws.request() do
{:error, _} ->
table_name()
|> Dynamo.create_table("key", %{key: :string}, 4, 1)
|> ExAws.request!()

_ ->
case table_name() |> Dynamo.describe_time_to_live() |> ExAws.request!() do
%{"TimeToLiveDescription" => %{"TimeToLiveStatus" => "DISABLED"}} ->
table_name()
|> Dynamo.update_time_to_live("expires_at", true)
|> ExAws.request!()
end
end

nil
end

def delete_table do
table_name()
|> Dynamo.delete_table()
|> ExAws.request()
end

def create_table? do
Config.dynamodb(:create_table, true)
end

defp table_name do
Config.dynamodb!(:table_name)
end

def key(client \\ :default_client, audience) do
"#{client}:#{audience}"
end
end
10 changes: 9 additions & 1 deletion lib/prima_auth0_ex/token_cache/token_cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ defmodule PrimaAuth0Ex.TokenCache do
@callback get_token_for(atom(), String.t()) :: {:ok, TokenInfo.t() | nil} | {:error, any()}
@callback child_spec(any()) :: Supervisor.child_spec()

@optional_callbacks child_spec: 1

def set_token_for(client, audience, token) do
get_configured_cache_provider().set_token_for(client, audience, token)
end
Expand All @@ -18,7 +20,13 @@ defmodule PrimaAuth0Ex.TokenCache do
end

def child_spec(opts) do
get_configured_cache_provider().child_spec(opts)
cache_provider = get_configured_cache_provider()

if function_exported?(cache_provider, :child_spec, 1) do
cache_provider.child_spec(opts)
else
[]
end
end

def get_configured_cache_provider do
Expand Down
5 changes: 3 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule PrimaAuth0Ex.MixProject do
use Mix.Project

@source_url "https://github.com/primait/auth0_ex"
@version "0.8.0"
@version "0.9.0"

def project do
[
Expand Down Expand Up @@ -41,7 +41,8 @@ defmodule PrimaAuth0Ex.MixProject do
{:redix, "~> 0.9 or ~> 1.0"},
{:telepoison, "~> 2.0"},
{:telemetry, "~> 1.0"},
{:timex, "~> 3.6"}
{:timex, "~> 3.6"},
{:ex_aws_dynamo, "~> 4.0"}
] ++ optional_deps() ++ dev_deps()
end

Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"ex_aws": {:hex, :ex_aws, "2.5.7", "dbcda183903cded392742129bd5c67ccf59caed4ded604d5e68b96e75570d743", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2c3c577550bfc4d0899e9fed9aeef91bc6a2aedd0177b1faa726c9b20d005074"},
"ex_aws_dynamo": {:hex, :ex_aws_dynamo, "4.2.2", "7f7975b14f9999749b1dfb5bfff87fd80367dffcc2fe2dfea5a540ac216f5fe3", [:mix], [{:ex_aws, ">= 2.4.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm", "e61ee3e6b9e25794592059cd81356ebfc57676d9ff82755316925bf7feca672e"},
"ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"},
"expo": {:hex, :expo, "0.4.0", "bbe4bf455e2eb2ebd2f1e7d83530ce50fb9990eb88fc47855c515bfdf1c6626f", [:mix], [], "hexpm", "a8ed1683ec8b7c7fa53fd7a41b2c6935f539168a6bb0616d7fd6b58a36f3abf2"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
Expand Down
26 changes: 26 additions & 0 deletions test/prima_auth0_ex/token_cache/dynamodb_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Integration.TokenCache.DynamoDBTest do
alias PrimaAuth0Ex.TokenCache.DynamoDB

use PrimaAuth0Ex.TestSupport.TokenCacheBehaviorCaseTemplate,
async: true,
cache_module: DynamoDB,
# Token expiration is managed by aws, and could take days for old tokens to be deleted,
# so we don't cover that in the tests here
test_token_expiration: false

setup do
cache_env = Application.get_env(:prima_auth0_ex, :dynamodb_cache)

on_exit(fn ->
if cache_env == nil do
Application.delete_env(:prima_auth0_ex, :dynamodb_cache)
else
Application.put_env(:prima_auth0_ex, :dynamodb_cache, cache_env)
end
end)

DynamoDB.delete_table()
start_supervised!(DynamoDB)
:ok
end
end
Loading

0 comments on commit b294331

Please sign in to comment.