Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PLATFORM-2332]: Implement caching using dynamo #251

Merged
merged 13 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}"
cpiemontese marked this conversation as resolved.
Show resolved Hide resolved
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)
cpiemontese marked this conversation as resolved.
Show resolved Hide resolved
else
Application.put_env(:prima_auth0_ex, :dynamodb_cache, cache_env)
end
end)

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