From c072a754c641ca5e94f77ac86371dfa07c223e5e Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:06:11 +0100 Subject: [PATCH 01/13] Setup dynamodb --- config/dev.exs | 11 ++++++++++- config/test.exs | 10 ++++++++++ docker-compose.yml | 12 ++++++++++-- mix.exs | 3 ++- mix.lock | 2 ++ 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/config/dev.exs b/config/dev.exs index 8796b895..72c50bf7 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -2,4 +2,13 @@ import Config config :prima_auth0_ex, :redis, enabled: false -config :logger, level: :debug +#config :logger, level: :debug +config :ex_aws, + access_key_id: "ABCD", + secret_access_key: "secret" + +config :ex_aws, :dynamodb, + scheme: "http://", + host: "dynamodb", + port: 8000, + region: "us-east-1" diff --git a/config/test.exs b/config/test.exs index 1f1b084d..8d415561 100644 --- a/config/test.exs +++ b/config/test.exs @@ -40,4 +40,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: "dynamodb", + port: 8000, + region: "us-east-1" + config :logger, level: :warning diff --git a/docker-compose.yml b/docker-compose.yml index d7791467..ff49d46a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,19 +21,27 @@ services: stdin_open: true depends_on: - redis + - dynamodb - localauth0 redis: image: public.ecr.aws/bitnami/redis:5.0 ports: - "6379:6379" - hostname: 'redis' + hostname: "redis" environment: - ALLOW_EMPTY_PASSWORD=yes + dynamodb: + # command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" + image: "amazon/dynamodb-local:latest" + container_name: dynamodb + ports: + - "8000:8000" + localauth0: image: public.ecr.aws/c6i9l4r6/localauth0:0.6.2 - ports: + ports: - 3000:3000 environment: LOCALAUTH0_CONFIG_PATH: /localauth0.toml diff --git a/mix.exs b/mix.exs index 61b65718..35b5a865 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock index bb47e0f2..54028fe6 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, From f88a2677a81d50639b849c2a9b32b921d33f5bdb Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:54:26 +0100 Subject: [PATCH 02/13] Extract cache behavior tests into a single module --- .../encrypted_redis_token_cache_test.exs | 61 ++++------------ .../token_cache/memory_cache_test.exs | 47 ++----------- .../token_cache/noop_cache_test.exs | 7 +- .../token_cache_behavior_case_template.ex | 70 +++++++++++++++++++ .../support/token_cache_behavior_test_case.ex | 2 + 5 files changed, 95 insertions(+), 92 deletions(-) create mode 100644 test/support/token_cache_behavior_case_template.ex create mode 100644 test/support/token_cache_behavior_test_case.ex diff --git a/test/prima_auth0_ex/token_cache/encrypted_redis_token_cache_test.exs b/test/prima_auth0_ex/token_cache/encrypted_redis_token_cache_test.exs index 43c3bd08..27054543 100644 --- a/test/prima_auth0_ex/token_cache/encrypted_redis_token_cache_test.exs +++ b/test/prima_auth0_ex/token_cache/encrypted_redis_token_cache_test.exs @@ -1,6 +1,4 @@ defmodule Integration.TokenCache.EncryptedRedisTokenCacheTest do - use ExUnit.Case, async: false - import ExUnit.CaptureLog import PrimaAuth0Ex.TestSupport.TimeUtils @@ -8,7 +6,9 @@ defmodule Integration.TokenCache.EncryptedRedisTokenCacheTest do alias PrimaAuth0Ex.TokenCache.EncryptedRedisTokenCache alias PrimaAuth0Ex.TokenProvider.TokenInfo - @test_audience "redis-integration-test-audience" + use PrimaAuth0Ex.TestSupport.TokenCacheBehaviorCaseTemplate, + async: false, + cache_module: EncryptedRedisTokenCache setup_all do start_supervised(EncryptedRedisTokenCache) @@ -16,7 +16,7 @@ defmodule Integration.TokenCache.EncryptedRedisTokenCacheTest do end setup do - Redix.command!(PrimaAuth0Ex.Redix, ["DEL", token_key(@test_audience)]) + Redix.command!(PrimaAuth0Ex.Redix, ["DEL", token_key(test_audience())]) redis_env = Application.fetch_env!(:prima_auth0_ex, :redis) cache_env = Application.fetch_env!(:prima_auth0_ex, :token_cache) @@ -34,11 +34,11 @@ defmodule Integration.TokenCache.EncryptedRedisTokenCacheTest do test "malformed token" do log = capture_log(fn -> - EncryptedRedisTokenCache.set_token_for(@test_audience, <<0x80>>) + EncryptedRedisTokenCache.set_token_for(test_audience(), <<0x80>>) end) assert String.match?(log, ~r/reason=/) - assert String.match?(log, ~r/audience=redis-integration-test-audience/) + assert String.contains?(log, "audience=#{test_audience()}") assert String.match?(log, ~r/Error setting token on redis./) end @@ -47,40 +47,29 @@ defmodule Integration.TokenCache.EncryptedRedisTokenCacheTest do log = capture_log(fn -> - EncryptedRedisTokenCache.set_token_for(@test_audience, sample_token()) + EncryptedRedisTokenCache.set_token_for(test_audience(), sample_token()) end) assert String.match?(log, ~r/reason=/) - assert String.match?(log, ~r/audience=redis-integration-test-audience/) + assert String.contains?(log, "audience=#{test_audience()}") assert String.match?(log, ~r/Error setting token on redis./) end end - test "persists and retrieves tokens" do - token = sample_token() - :ok = EncryptedRedisTokenCache.set_token_for(@test_audience, token) - - assert {:ok, token} == EncryptedRedisTokenCache.get_token_for(@test_audience) - end - test "retrieves tokens set by a previous version of prima_auth0_ex, hence without kid" do issued_at = one_hour_ago() expires_at = in_one_hour() token_without_kid = %{jwt: "my-token", issued_at: issued_at, expires_at: expires_at} - :ok = EncryptedRedisTokenCache.set_token_for(@test_audience, token_without_kid) + :ok = EncryptedRedisTokenCache.set_token_for(test_audience(), token_without_kid) assert {:ok, %TokenInfo{jwt: "my-token", issued_at: ^issued_at, expires_at: ^expires_at, kid: nil}} = - EncryptedRedisTokenCache.get_token_for(@test_audience) - end - - test "returns {:ok, nil} when token is not cached" do - assert {:ok, nil} == EncryptedRedisTokenCache.get_token_for(@test_audience) + EncryptedRedisTokenCache.get_token_for(test_audience()) end test "encrypts tokens" do - :ok = EncryptedRedisTokenCache.set_token_for(@test_audience, sample_token()) + :ok = EncryptedRedisTokenCache.set_token_for(test_audience(), sample_token()) - persisted_token = Redix.command!(PrimaAuth0Ex.Redix, ["GET", token_key(@test_audience)]) + persisted_token = Redix.command!(PrimaAuth0Ex.Redix, ["GET", token_key(test_audience())]) assert is_binary(persisted_token) assert {:error, _} = Jason.decode(persisted_token) @@ -91,33 +80,11 @@ defmodule Integration.TokenCache.EncryptedRedisTokenCacheTest do # this may happen e.g., if the secret key changes Redix.command!(PrimaAuth0Ex.Redix, [ "SET", - token_key(@test_audience), + token_key(test_audience()), "malformed-encrypted-token" ]) - assert {:error, _} = EncryptedRedisTokenCache.get_token_for(@test_audience) - end - - test "tokens are deleted from cache when they expire" do - token = %TokenInfo{sample_token() | expires_at: shifted_by_seconds(2)} - :ok = EncryptedRedisTokenCache.set_token_for(@test_audience, token) - - # Token shouldn't have expired yet - :timer.sleep(1000) - assert {:ok, ^token} = EncryptedRedisTokenCache.get_token_for(@test_audience) - - # Token expired - :timer.sleep(2100) - assert {:ok, nil} = EncryptedRedisTokenCache.get_token_for(@test_audience) - end - - defp sample_token do - %TokenInfo{ - jwt: "my-token", - issued_at: one_hour_ago(), - expires_at: in_one_hour(), - kid: "my-kid" - } + assert {:error, _} = EncryptedRedisTokenCache.get_token_for(test_audience()) end defp token_key(audience), do: "prima_auth0_ex_tokens:#{namespace()}:#{audience}" diff --git a/test/prima_auth0_ex/token_cache/memory_cache_test.exs b/test/prima_auth0_ex/token_cache/memory_cache_test.exs index 46d616f4..eadb74ef 100644 --- a/test/prima_auth0_ex/token_cache/memory_cache_test.exs +++ b/test/prima_auth0_ex/token_cache/memory_cache_test.exs @@ -1,13 +1,10 @@ defmodule Integration.TokenCache.MemoryCacheTest do - use ExUnit.Case, async: true - - import PrimaAuth0Ex.TestSupport.TimeUtils - alias PrimaAuth0Ex.TokenCache.MemoryCache - alias PrimaAuth0Ex.TokenProvider.TokenInfo - @client :memory_cache_client - @test_audience "memory-cache-test-audience" + use PrimaAuth0Ex.TestSupport.TokenCacheBehaviorCaseTemplate, async: true, cache_module: MemoryCache + setup_all do + Application.put_env(:prima_auth0_ex, :memory_cache, cleanup_interval: 25) + end setup do cache_env = Application.get_env(:prima_auth0_ex, :memory_cache) @@ -23,40 +20,4 @@ defmodule Integration.TokenCache.MemoryCacheTest do start_supervised!(MemoryCache) :ok end - - test "persists and retrieves tokens" do - token = sample_token() - :ok = MemoryCache.set_token_for(@client, @test_audience, token) - - assert {:ok, token} == MemoryCache.get_token_for(@client, @test_audience) - end - - test "returns {:ok, nil} when token is not cached" do - assert {:ok, nil} == MemoryCache.get_token_for(@client, @test_audience) - end - - test "tokens are deleted from cache when they expire" do - Application.put_env(:prima_auth0_ex, :memory_cache, cleanup_interval: 25) - stop_supervised!(MemoryCache) - start_supervised!(MemoryCache) - - token = %TokenInfo{sample_token() | expires_at: shifted_by_seconds(2)} - :ok = MemoryCache.set_token_for(@client, @test_audience, token) - :timer.sleep(1000) - # Token shouldn't have expired yet - assert {:ok, ^token} = MemoryCache.get_token_for(@client, @test_audience) - - :timer.sleep(2100) - # Token expired - assert {:ok, nil} = MemoryCache.get_token_for(@client, @test_audience) - end - - defp sample_token do - %TokenInfo{ - jwt: "my-token", - issued_at: one_hour_ago(), - expires_at: in_one_hour(), - kid: "my-kid" - } - end end diff --git a/test/prima_auth0_ex/token_cache/noop_cache_test.exs b/test/prima_auth0_ex/token_cache/noop_cache_test.exs index 7ddfc9b7..7ad4dcc4 100644 --- a/test/prima_auth0_ex/token_cache/noop_cache_test.exs +++ b/test/prima_auth0_ex/token_cache/noop_cache_test.exs @@ -1,10 +1,13 @@ defmodule PrimaAuth0Ex.TokenCache.NoopCacheTest do - use ExUnit.Case, async: true - import PrimaAuth0Ex.TestSupport.TimeUtils alias PrimaAuth0Ex.TokenCache.NoopCache alias PrimaAuth0Ex.TokenProvider.TokenInfo + use PrimaAuth0Ex.TestSupport.TokenCacheBehaviorCaseTemplate, + cache_module: NoopCache, + test_persists_tokens: false + + test "doesn't store tokens" do cached_token = %TokenInfo{jwt: "CACHED-TOKEN", issued_at: one_hour_ago(), expires_at: in_one_hour()} :ok = NoopCache.set_token_for(:noop_test_client, "audience", cached_token) diff --git a/test/support/token_cache_behavior_case_template.ex b/test/support/token_cache_behavior_case_template.ex new file mode 100644 index 00000000..a4c7ca1d --- /dev/null +++ b/test/support/token_cache_behavior_case_template.ex @@ -0,0 +1,70 @@ +defmodule PrimaAuth0Ex.TestSupport.TokenCacheBehaviorCaseTemplate do + use ExUnit.CaseTemplate + import PrimaAuth0Ex.TestSupport.TimeUtils + + alias PrimaAuth0Ex.TokenProvider.TokenInfo + + using options do + cache_module = Keyword.fetch!(options, :cache_module) + # Whether this behavior actually persists tokens. + # Probably only useful for the noop token cache + persists_tokens = Keyword.get(options, :test_persists_tokens, true) + # Whether this behavior expires tokens automatically + expires_tokens = Keyword.get(options, :test_token_expiration, persists_tokens) + + quote do + defp sample_token do + %TokenInfo{ + jwt: "my-token", + issued_at: one_hour_ago(), + expires_at: in_one_hour(), + kid: "my-kid" + } + end + + def cache_module do + unquote(cache_module) + end + + def client_name do + :default_client + end + + def test_audience do + mod = "ASDF" + test_name = "BSDF" + "#{Macro.underscore(mod)}:#{test_name}" + end + + describe "token cache" do + @tag :asdf + test "returns {:ok, nil} when token is not cached" do + assert {:ok, nil} == cache_module().get_token_for(client_name(), test_audience()) + end + + if unquote(persists_tokens) do + test "persists and retrieves tokens" do + token = sample_token() + :ok = cache_module().set_token_for(client_name(), test_audience(), token) + + assert {:ok, token} == cache_module().get_token_for(client_name(), test_audience()) + end + end + + if unquote(expires_tokens) do + test "tokens are purged from cache when they expire" do + token = %TokenInfo{sample_token() | expires_at: shifted_by_seconds(2)} + :ok = cache_module().set_token_for(client_name(), test_audience(), token) + :timer.sleep(1000) + # Token shouldn't have expired yet + assert {:ok, ^token} = cache_module().get_token_for(client_name(), test_audience()) + + :timer.sleep(2100) + # Token expired + assert {:ok, nil} = cache_module().get_token_for(client_name(), test_audience()) + end + end + end + end + end +end diff --git a/test/support/token_cache_behavior_test_case.ex b/test/support/token_cache_behavior_test_case.ex new file mode 100644 index 00000000..486cf32c --- /dev/null +++ b/test/support/token_cache_behavior_test_case.ex @@ -0,0 +1,2 @@ +defmodule PrimaAuth0Ex.TestSupport.TokenCacheBehaviorTestCase do +end From a501671ed0fc7c7a2e8884790a3d0233716df839 Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Tue, 26 Nov 2024 22:43:29 +0100 Subject: [PATCH 03/13] Add dynamodb cache --- config/dev.exs | 3 +- config/test.exs | 4 +- docker-compose.yml | 16 +-- lib/prima_auth0_ex/config.ex | 3 + lib/prima_auth0_ex/token_cache/dynamodb.ex | 105 ++++++++++++++++++ lib/prima_auth0_ex/token_cache/token_cache.ex | 10 +- .../token_cache/dynamodb_test.exs | 25 +++++ 7 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 lib/prima_auth0_ex/token_cache/dynamodb.ex create mode 100644 test/prima_auth0_ex/token_cache/dynamodb_test.exs diff --git a/config/dev.exs b/config/dev.exs index 72c50bf7..c8bf85f4 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -2,7 +2,8 @@ import Config config :prima_auth0_ex, :redis, enabled: false -#config :logger, level: :debug +config :logger, level: :debug + config :ex_aws, access_key_id: "ABCD", secret_access_key: "secret" diff --git a/config/test.exs b/config/test.exs index 8d415561..88993498 100644 --- a/config/test.exs +++ b/config/test.exs @@ -46,8 +46,8 @@ config :ex_aws, config :ex_aws, :dynamodb, scheme: "http://", - host: "dynamodb", - port: 8000, + host: "aws", + port: 4566, region: "us-east-1" config :logger, level: :warning diff --git a/docker-compose.yml b/docker-compose.yml index ff49d46a..cba1ab25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: stdin_open: true depends_on: - redis - - dynamodb + - aws - localauth0 redis: @@ -32,13 +32,6 @@ services: environment: - ALLOW_EMPTY_PASSWORD=yes - dynamodb: - # command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" - image: "amazon/dynamodb-local:latest" - container_name: dynamodb - ports: - - "8000:8000" - localauth0: image: public.ecr.aws/c6i9l4r6/localauth0:0.6.2 ports: @@ -48,5 +41,12 @@ services: volumes: - ./localauth0.toml:/localauth0.toml:ro + aws: + image: public.ecr.aws/localstack/localstack:4 + ports: + - "4566:4566" + environment: + ALLOW_NONSTANDARD_REGIONS: 1 + volumes: app: diff --git a/lib/prima_auth0_ex/config.ex b/lib/prima_auth0_ex/config.ex index 39d007f6..d44c00c7 100644 --- a/lib/prima_auth0_ex/config.ex +++ b/lib/prima_auth0_ex/config.ex @@ -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) diff --git a/lib/prima_auth0_ex/token_cache/dynamodb.ex b/lib/prima_auth0_ex/token_cache/dynamodb.ex new file mode 100644 index 00000000..bdec1a9d --- /dev/null +++ b/lib/prima_auth0_ex/token_cache/dynamodb.ex @@ -0,0 +1,105 @@ +defmodule PrimaAuth0Ex.TokenCache.DynamoDB.StoredToken do + 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() + } +end + +defmodule PrimaAuth0Ex.TokenCache.DynamoDB do + alias PrimaAuth0Ex.TokenCache.DynamoDB.StoredToken + alias PrimaAuth0Ex.TokenCache + alias PrimaAuth0Ex.Config + alias PrimaAuth0Ex.TokenProvider.TokenInfo + alias ExAws.Dynamo + + @behaviour TokenCache + + @impl TokenCache + def child_spec(_), + do: %{ + id: __MODULE__, + start: {__MODULE__, :start, []}, + restart: :transient + } + + def start do + if create_table?() do + create_update_table() + end + + :ignore + end + + @impl TokenCache + 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{issued_at: issued_at, expires_at: expires_at, jwt: jwt, kid: kid} <- + Dynamo.decode_item(res, as: StoredToken) do + {:ok, + %TokenInfo{ + jwt: jwt, + kid: kid, + expires_at: expires_at, + issued_at: issued_at + }} + else + {:ok, %{}} -> {:ok, nil} + {:error, error} -> {:error, error} + end + end + + @impl TokenCache + def set_token_for( + client \\ :default_client, + audience, + %TokenInfo{expires_at: expires_at, issued_at: issued_at, kid: kid, jwt: jwt} = token_info + ) do + stored_token = %StoredToken{ + key: key(client, audience), + expires_at: expires_at, + issued_at: issued_at, + kid: kid, + jwt: jwt + } + + case Dynamo.put_item(table_name(), stored_token) |> ExAws.request() do + {:ok, _} -> :ok + {:error, err} -> {:error, err} + end + end + + def create_update_table() do + if {:error, _} = Dynamo.describe_table(table_name()) |> ExAws.request() do + Dynamo.create_table(table_name(), "key", %{key: :string}, 4, 1) + |> ExAws.request!() + end + + Dynamo.update_time_to_live(table_name(), "expires_at", true) + |> ExAws.request!() + end + + def delete_table() do + Dynamo.delete_table(table_name()) + |> 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 diff --git a/lib/prima_auth0_ex/token_cache/token_cache.ex b/lib/prima_auth0_ex/token_cache/token_cache.ex index 693242a3..2bec3dc8 100644 --- a/lib/prima_auth0_ex/token_cache/token_cache.ex +++ b/lib/prima_auth0_ex/token_cache/token_cache.ex @@ -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 @@ -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 diff --git a/test/prima_auth0_ex/token_cache/dynamodb_test.exs b/test/prima_auth0_ex/token_cache/dynamodb_test.exs new file mode 100644 index 00000000..733f8586 --- /dev/null +++ b/test/prima_auth0_ex/token_cache/dynamodb_test.exs @@ -0,0 +1,25 @@ +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() + DynamoDB.init() + end +end From a40560743eaec3c02164cbfb9464908ed6c9633c Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Tue, 26 Nov 2024 22:54:48 +0100 Subject: [PATCH 04/13] format --- test/prima_auth0_ex/token_cache/memory_cache_test.exs | 1 + test/prima_auth0_ex/token_cache/noop_cache_test.exs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/test/prima_auth0_ex/token_cache/memory_cache_test.exs b/test/prima_auth0_ex/token_cache/memory_cache_test.exs index eadb74ef..f6d7b482 100644 --- a/test/prima_auth0_ex/token_cache/memory_cache_test.exs +++ b/test/prima_auth0_ex/token_cache/memory_cache_test.exs @@ -2,6 +2,7 @@ defmodule Integration.TokenCache.MemoryCacheTest do alias PrimaAuth0Ex.TokenCache.MemoryCache use PrimaAuth0Ex.TestSupport.TokenCacheBehaviorCaseTemplate, async: true, cache_module: MemoryCache + setup_all do Application.put_env(:prima_auth0_ex, :memory_cache, cleanup_interval: 25) end diff --git a/test/prima_auth0_ex/token_cache/noop_cache_test.exs b/test/prima_auth0_ex/token_cache/noop_cache_test.exs index 7ad4dcc4..696b6d2f 100644 --- a/test/prima_auth0_ex/token_cache/noop_cache_test.exs +++ b/test/prima_auth0_ex/token_cache/noop_cache_test.exs @@ -7,7 +7,6 @@ defmodule PrimaAuth0Ex.TokenCache.NoopCacheTest do cache_module: NoopCache, test_persists_tokens: false - test "doesn't store tokens" do cached_token = %TokenInfo{jwt: "CACHED-TOKEN", issued_at: one_hour_ago(), expires_at: in_one_hour()} :ok = NoopCache.set_token_for(:noop_test_client, "audience", cached_token) From ed032162734b82fc11b9bcd9ccf514801167c2c0 Mon Sep 17 00:00:00 2001 From: MaeIsBad <26093674+MaeIsBad@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:54:57 +0100 Subject: [PATCH 05/13] Update config/dev.exs Co-authored-by: Cristiano Piemontese --- config/dev.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/dev.exs b/config/dev.exs index c8bf85f4..7a2b03ce 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -11,5 +11,5 @@ config :ex_aws, config :ex_aws, :dynamodb, scheme: "http://", host: "dynamodb", - port: 8000, + port: 4566, region: "us-east-1" From 9f01cef9065f608ed6249e50891abde007d7e6c5 Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:13:05 +0100 Subject: [PATCH 06/13] Extract Dynamodb.StoredToken TokenInfo conversion functions --- lib/prima_auth0_ex/token_cache/dynamodb.ex | 39 +++++++++++++--------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/prima_auth0_ex/token_cache/dynamodb.ex b/lib/prima_auth0_ex/token_cache/dynamodb.ex index bdec1a9d..fd238527 100644 --- a/lib/prima_auth0_ex/token_cache/dynamodb.ex +++ b/lib/prima_auth0_ex/token_cache/dynamodb.ex @@ -10,6 +10,25 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB.StoredToken do 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 @@ -41,15 +60,9 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do 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{issued_at: issued_at, expires_at: expires_at, jwt: jwt, kid: kid} <- + %StoredToken{} = stored_token <- Dynamo.decode_item(res, as: StoredToken) do - {:ok, - %TokenInfo{ - jwt: jwt, - kid: kid, - expires_at: expires_at, - issued_at: issued_at - }} + {:ok, StoredToken.to_token_info(stored_token)} else {:ok, %{}} -> {:ok, nil} {:error, error} -> {:error, error} @@ -60,15 +73,9 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do def set_token_for( client \\ :default_client, audience, - %TokenInfo{expires_at: expires_at, issued_at: issued_at, kid: kid, jwt: jwt} = token_info + %TokenInfo{} = token_info ) do - stored_token = %StoredToken{ - key: key(client, audience), - expires_at: expires_at, - issued_at: issued_at, - kid: kid, - jwt: jwt - } + stored_token = StoredToken.from_token_info(key(client, audience), token_info) case Dynamo.put_item(table_name(), stored_token) |> ExAws.request() do {:ok, _} -> :ok From 32336e2a88778edf711eee5f61389fcfec31579d Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:00:52 +0100 Subject: [PATCH 07/13] Linters --- lib/prima_auth0_ex/token_cache/dynamodb.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/prima_auth0_ex/token_cache/dynamodb.ex b/lib/prima_auth0_ex/token_cache/dynamodb.ex index fd238527..634d49fb 100644 --- a/lib/prima_auth0_ex/token_cache/dynamodb.ex +++ b/lib/prima_auth0_ex/token_cache/dynamodb.ex @@ -57,6 +57,10 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do 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), @@ -83,6 +87,8 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do end end + # More ExAws typing issues + @dialyzer {:nowarn_function, create_update_table: 0} def create_update_table() do if {:error, _} = Dynamo.describe_table(table_name()) |> ExAws.request() do Dynamo.create_table(table_name(), "key", %{key: :string}, 4, 1) @@ -91,6 +97,8 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do Dynamo.update_time_to_live(table_name(), "expires_at", true) |> ExAws.request!() + + nil end def delete_table() do From 47a9b279aa51507e048ff8e3e3a8b0e5fc4044f5 Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:39:18 +0100 Subject: [PATCH 08/13] Fix tests --- config/test.exs | 2 ++ lib/prima_auth0_ex/token_cache/dynamodb.ex | 17 +++++++++++------ .../token_cache/dynamodb_test.exs | 3 ++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/config/test.exs b/config/test.exs index 88993498..bdd22746 100644 --- a/config/test.exs +++ b/config/test.exs @@ -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", diff --git a/lib/prima_auth0_ex/token_cache/dynamodb.ex b/lib/prima_auth0_ex/token_cache/dynamodb.ex index 634d49fb..7033ba0e 100644 --- a/lib/prima_auth0_ex/token_cache/dynamodb.ex +++ b/lib/prima_auth0_ex/token_cache/dynamodb.ex @@ -41,12 +41,13 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do @behaviour TokenCache @impl TokenCache - def child_spec(_), - do: %{ + def child_spec(_) do + %{ id: __MODULE__, start: {__MODULE__, :start, []}, restart: :transient } + end def start do if create_table?() do @@ -90,9 +91,13 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do # More ExAws typing issues @dialyzer {:nowarn_function, create_update_table: 0} def create_update_table() do - if {:error, _} = Dynamo.describe_table(table_name()) |> ExAws.request() do - Dynamo.create_table(table_name(), "key", %{key: :string}, 4, 1) - |> ExAws.request!() + case Dynamo.describe_table(table_name()) |> ExAws.request() do + {:error, _} -> + Dynamo.create_table(table_name(), "key", %{key: :string}, 4, 1) + |> ExAws.request!() + + _ -> + nil end Dynamo.update_time_to_live(table_name(), "expires_at", true) @@ -103,7 +108,7 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do def delete_table() do Dynamo.delete_table(table_name()) - |> ExAws.request!() + |> ExAws.request() end def create_table? do diff --git a/test/prima_auth0_ex/token_cache/dynamodb_test.exs b/test/prima_auth0_ex/token_cache/dynamodb_test.exs index 733f8586..df008e7a 100644 --- a/test/prima_auth0_ex/token_cache/dynamodb_test.exs +++ b/test/prima_auth0_ex/token_cache/dynamodb_test.exs @@ -20,6 +20,7 @@ defmodule Integration.TokenCache.DynamoDBTest do end) DynamoDB.delete_table() - DynamoDB.init() + start_supervised!(DynamoDB) + :ok end end From ea363eb4a646ce93aeeddf512abfbbe15331e9e6 Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:34:59 +0100 Subject: [PATCH 09/13] Fix TimeToLive --- lib/prima_auth0_ex/token_cache/dynamodb.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/prima_auth0_ex/token_cache/dynamodb.ex b/lib/prima_auth0_ex/token_cache/dynamodb.ex index 7033ba0e..e826dbe7 100644 --- a/lib/prima_auth0_ex/token_cache/dynamodb.ex +++ b/lib/prima_auth0_ex/token_cache/dynamodb.ex @@ -95,14 +95,14 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do {:error, _} -> Dynamo.create_table(table_name(), "key", %{key: :string}, 4, 1) |> ExAws.request!() - _ -> - nil + case Dynamo.describe_time_to_live(table_name()) |> ExAws.request!() do + %{"TimeToLiveDescription" => %{"TimeToLiveStatus" => "DISABLED"}} -> + Dynamo.update_time_to_live(table_name(), "expires_at", true) + |> ExAws.request!() + end end - Dynamo.update_time_to_live(table_name(), "expires_at", true) - |> ExAws.request!() - nil end From 2a2ec8574c3c98944e8bd15119fd7b41dbe30e4f Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:53:33 +0100 Subject: [PATCH 10/13] Linters --- lib/prima_auth0_ex/token_cache/dynamodb.ex | 33 ++++++++++++------- .../token_cache_behavior_case_template.ex | 2 ++ .../support/token_cache_behavior_test_case.ex | 2 -- 3 files changed, 24 insertions(+), 13 deletions(-) delete mode 100644 test/support/token_cache_behavior_test_case.ex diff --git a/lib/prima_auth0_ex/token_cache/dynamodb.ex b/lib/prima_auth0_ex/token_cache/dynamodb.ex index e826dbe7..a384f133 100644 --- a/lib/prima_auth0_ex/token_cache/dynamodb.ex +++ b/lib/prima_auth0_ex/token_cache/dynamodb.ex @@ -1,4 +1,6 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB.StoredToken do + @moduledoc false + alias PrimaAuth0Ex.TokenProvider.TokenInfo @derive [ExAws.Dynamo.Encodable] defstruct [:key, :jwt, :issued_at, :expires_at, :kid] @@ -32,11 +34,16 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB.StoredToken do end defmodule PrimaAuth0Ex.TokenCache.DynamoDB do - alias PrimaAuth0Ex.TokenCache.DynamoDB.StoredToken - alias PrimaAuth0Ex.TokenCache + @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 - alias ExAws.Dynamo @behaviour TokenCache @@ -82,7 +89,7 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do ) do stored_token = StoredToken.from_token_info(key(client, audience), token_info) - case Dynamo.put_item(table_name(), stored_token) |> ExAws.request() do + case table_name() |> Dynamo.put_item(stored_token) |> ExAws.request() do {:ok, _} -> :ok {:error, err} -> {:error, err} end @@ -90,15 +97,18 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do # More ExAws typing issues @dialyzer {:nowarn_function, create_update_table: 0} - def create_update_table() do - case Dynamo.describe_table(table_name()) |> ExAws.request() do + def create_update_table do + case table_name() |> Dynamo.describe_table() |> ExAws.request() do {:error, _} -> - Dynamo.create_table(table_name(), "key", %{key: :string}, 4, 1) + table_name() + |> Dynamo.create_table("key", %{key: :string}, 4, 1) |> ExAws.request!() + _ -> - case Dynamo.describe_time_to_live(table_name()) |> ExAws.request!() do + case table_name() |> Dynamo.describe_time_to_live() |> ExAws.request!() do %{"TimeToLiveDescription" => %{"TimeToLiveStatus" => "DISABLED"}} -> - Dynamo.update_time_to_live(table_name(), "expires_at", true) + table_name() + |> Dynamo.update_time_to_live("expires_at", true) |> ExAws.request!() end end @@ -106,8 +116,9 @@ defmodule PrimaAuth0Ex.TokenCache.DynamoDB do nil end - def delete_table() do - Dynamo.delete_table(table_name()) + def delete_table do + table_name() + |> Dynamo.delete_table() |> ExAws.request() end diff --git a/test/support/token_cache_behavior_case_template.ex b/test/support/token_cache_behavior_case_template.ex index a4c7ca1d..faf74fb8 100644 --- a/test/support/token_cache_behavior_case_template.ex +++ b/test/support/token_cache_behavior_case_template.ex @@ -1,4 +1,6 @@ defmodule PrimaAuth0Ex.TestSupport.TokenCacheBehaviorCaseTemplate do + @moduledoc false + use ExUnit.CaseTemplate import PrimaAuth0Ex.TestSupport.TimeUtils diff --git a/test/support/token_cache_behavior_test_case.ex b/test/support/token_cache_behavior_test_case.ex deleted file mode 100644 index 486cf32c..00000000 --- a/test/support/token_cache_behavior_test_case.ex +++ /dev/null @@ -1,2 +0,0 @@ -defmodule PrimaAuth0Ex.TestSupport.TokenCacheBehaviorTestCase do -end From 2f0682e7442d62debfad5ac0bcf049c45f1e6ef4 Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:13:14 +0100 Subject: [PATCH 11/13] Fix CI --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7db0a629..80856b88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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() From 91f12f6076f1fdc761589214a6e7f5a339e6fb93 Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:41:44 +0100 Subject: [PATCH 12/13] Document dynamodb cache --- CHANGELOG.md | 4 ++++ README.md | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59787be5..2af0d0f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +# Added + +A new DynamoDB cache provider + --- ## [0.8.0] - 2024-11-29 diff --git a/README.md b/README.md index 9337e9a6..64441775 100644 --- a/README.md +++ b/README.md @@ -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: From c118177d7584a205a39cee114c92fb214c384745 Mon Sep 17 00:00:00 2001 From: mae <26093674+MaeIsBad@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:42:03 +0100 Subject: [PATCH 13/13] Bump --- CHANGELOG.md | 8 +++++++- mix.exs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af0d0f8..df61008a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +--- + +## [0.9.0] - 2024-12-04 + # Added A new DynamoDB cache provider @@ -309,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 diff --git a/mix.exs b/mix.exs index 35b5a865..400bde79 100644 --- a/mix.exs +++ b/mix.exs @@ -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 [