diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 991b4dc5..62f4bc4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,14 +11,30 @@ on: jobs: ci: runs-on: ubuntu-latest - container: - image: elixir:1.13 env: MIX_ENV: test steps: + - uses: erlef/setup-beam@v1 + with: + elixir-version: 1.13 + otp-version: 24 + # Check out the code. - name: Checkout uses: actions/checkout@v3 + + - name: Restart localuth0 + # Restart localauth0 after checking out so it can load the config + run: docker restart localauth0 + + - name: Add hosts entries + run: | + echo " + 127.0.0.1 localauth0 + 127.0.0.1 redis + " | sudo tee /etc/hosts + + # Define how to cache deps. Restores existing cache if present. - name: Cache deps id: cache-deps @@ -53,10 +69,6 @@ jobs: run: | mix deps.clean --all mix clean - - name: Elixir setup - run: | - mix local.hex --force - mix local.rebar --force - name: Deps get run: mix deps.get - name: Dependencies Check @@ -85,6 +97,16 @@ jobs: ports: - 6379:6379 + localauth0: + image: public.ecr.aws/c6i9l4r6/localauth0:0.6.2 + ports: + - 3000:3000 + env: + LOCALAUTH0_CONFIG_PATH: /repo/localauth0.toml + volumes: + - ./:/repo:ro + options: --name localauth0 + alls-green: if: always() needs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 966664af..572ff8a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Deprecate the `:prima_auth0_ex, :redis, :enabled` option in favor of `:prima_auth0_ex, :token_cache` -To migrate set `:prima_auth0_ex, :token_cache` to `EncryptedRedisTokenCache` or `NoopCache` +To migrate set `:prima_auth0_ex, :token_cache` to `EncryptedRedisTokenCache`, `NoopCache` or `MemoryCache`(default) + +- Tokens are now cached by default using the MemoryCache backend + +You can disable it by setting `:prima_auth0_ex, :token_cache` to `NoopCache`. + +- Use the [rfc8414](https://www.rfc-editor.org/rfc/rfc8414) metadata endpoint to fetch information about the auth server. + +This allows auth0_ex to be used with other compliant openid servers, like okta. + +Note that if you're using [localauth0](https://github.com/primait/localauth0), you will need to update to version 0.6.2 or later(public.ecr.aws/c6i9l4r6/localauth0:0.6.2). + +### Changed + +- Use the [rfc8414](https://www.rfc-editor.org/rfc/rfc8414) metadata endpoint to fetch information about the auth server + +This allows auth0_ex to be used with other compliant openid servers, like okta. + +Note that if you're using [localauth0](https://github.com/primait/localauth0), you will need to update to version 0.6.2 or later(public.ecr.aws/c6i9l4r6/localauth0:0.6.2). --- diff --git a/README.md b/README.md index c0023f1a..36093087 100644 --- a/README.md +++ b/README.md @@ -242,13 +242,6 @@ The test suite can be executed as follows: mix test ``` -By default tests that integrate with Auth0 are excluded. -To run them, configure your auth0 credentials and audience in `config/test.exs` and run: - -```bash -mix test --include external -``` - Always run formatter, linter and dialyzer before pushing changes: ```bash diff --git a/config/config.exs b/config/config.exs index e1d36e37..7312691e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -3,9 +3,9 @@ import Config # Default client, for backwards compatibility config :prima_auth0_ex, :clients, default_client: [ - auth0_base_url: "https://your-auth0-provider.com", - client_id: "", - client_secret: "", + auth0_base_url: "http://localauth0:3000", + client_id: "client_id", + client_secret: "client_secret", cache_namespace: "my-service", token_check_interval: :timer.seconds(1), signature_check_interval: :timer.seconds(1) @@ -20,7 +20,7 @@ config :prima_auth0_ex, :redis, ssl_allow_wildcard_certificates: false config :prima_auth0_ex, :server, - auth0_base_url: "https://your-auth0-provider.com", + auth0_base_url: "http://localauth0:3000", ignore_signature: false, audience: "some-audience", issuer: "https://your-auth0-tenant.com", diff --git a/config/test.exs b/config/test.exs index 336127fd..abe706d6 100644 --- a/config/test.exs +++ b/config/test.exs @@ -8,10 +8,10 @@ config :prima_auth0_ex, token_service: TokenServiceMock config :prima_auth0_ex, :server, - auth0_base_url: "server", + auth0_base_url: "http://localauth0:3000", ignore_signature: false, audience: "server", - issuer: "server", + issuer: "https://your-auth0-tenant.com", first_jwks_fetch_sync: true config :prima_auth0_ex, :redis, @@ -22,9 +22,9 @@ config :prima_auth0_ex, :redis, config :prima_auth0_ex, :clients, default_client: [ - auth0_base_url: "default", - client_id: "default", - client_secret: "default", + auth0_base_url: "http://localauth0:3000", + client_id: "client_id", + client_secret: "client_secret", cache_namespace: "default", token_check_interval: :timer.seconds(1), signature_check_interval: :timer.seconds(1) diff --git a/docker-compose.yml b/docker-compose.yml index f72e3ce2..d7791467 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,7 @@ services: stdin_open: true depends_on: - redis + - localauth0 redis: image: public.ecr.aws/bitnami/redis:5.0 @@ -30,5 +31,14 @@ services: environment: - ALLOW_EMPTY_PASSWORD=yes + localauth0: + image: public.ecr.aws/c6i9l4r6/localauth0:0.6.2 + ports: + - 3000:3000 + environment: + LOCALAUTH0_CONFIG_PATH: /localauth0.toml + volumes: + - ./localauth0.toml:/localauth0.toml:ro + volumes: app: diff --git a/lib/prima_auth0_ex/jwks_strategy.ex b/lib/prima_auth0_ex/jwks_strategy.ex index d88267bc..a8849ce6 100644 --- a/lib/prima_auth0_ex/jwks_strategy.ex +++ b/lib/prima_auth0_ex/jwks_strategy.ex @@ -5,13 +5,11 @@ defmodule PrimaAuth0Ex.JwksStrategy do alias PrimaAuth0Ex.Config + alias PrimaAuth0Ex.OpenIDConfiguration use JokenJwks.DefaultStrategyTemplate def init_opts(opts) do - Keyword.merge(opts, jwks_url: jwks_url()) + jwks_url = Config.server(:auth0_base_url) |> OpenIDConfiguration.fetch!() |> Map.fetch!(:jwks_uri) + Keyword.merge(opts, jwks_url: jwks_url) end - - defp jwks_url, do: base_url() <> "/.well-known/jwks.json" - - defp base_url, do: Config.server!(:auth0_base_url) end diff --git a/lib/prima_auth0_ex/openid_configuration.ex b/lib/prima_auth0_ex/openid_configuration.ex new file mode 100644 index 00000000..4f837cea --- /dev/null +++ b/lib/prima_auth0_ex/openid_configuration.ex @@ -0,0 +1,40 @@ +defmodule PrimaAuth0Ex.OpenIDConfiguration do + @moduledoc false + @doc """ + Module for fetching and parsing the [rfc8414](https://www.rfc-editor.org/rfc/rfc8414) openid metadata endpoint + + This allows auth0_ex to be agnostic of the actual openid server + """ + + @type t :: %__MODULE__{issuer: String.t(), token_endpoint: String.t(), jwks_uri: String.t()} + + @struct_keys [:issuer, :token_endpoint, :jwks_uri] + defstruct @struct_keys + + @doc """ + Fetches the openid metadata. + + Doesn't implement caching and always makes a http request, avoid calling this in hot code paths + """ + @spec fetch!(String.t()) :: __MODULE__.t() + def fetch!(base_url) do + url = metadata_url(base_url) + %HTTPoison.Response{status_code: status_code, body: meta_body} = Telepoison.get!(url, accept: "application/json") + + unless status_code in 200..299 do + raise """ + Failed to retrieve the openid-configuration from #{url}, server sent ${status_code}: + #{meta_body} + + This is most likely caused by an incorrect base_url. + """ + end + + metadata = Jason.decode!(meta_body) + metadata = Map.new(@struct_keys, fn key -> {key, metadata[Atom.to_string(key)]} end) + + struct!(__MODULE__, metadata) + end + + defp metadata_url(base_url), do: base_url <> "/.well-known/openid-configuration" +end diff --git a/lib/prima_auth0_ex/token_cache/token_cache.ex b/lib/prima_auth0_ex/token_cache/token_cache.ex index 285b7489..693242a3 100644 --- a/lib/prima_auth0_ex/token_cache/token_cache.ex +++ b/lib/prima_auth0_ex/token_cache/token_cache.ex @@ -22,7 +22,7 @@ defmodule PrimaAuth0Ex.TokenCache do end def get_configured_cache_provider do - cache_provider = Config.token_cache(NoopCache) + cache_provider = Config.token_cache(MemoryCache) with builtin_cache_provider <- Module.concat(PrimaAuth0Ex.TokenCache, cache_provider), {:module, ^builtin_cache_provider} <- Code.ensure_compiled(builtin_cache_provider) do diff --git a/lib/prima_auth0_ex/token_provider/auth0_authorization_service.ex b/lib/prima_auth0_ex/token_provider/auth0_authorization_service.ex index b1bdaeba..de0ab809 100644 --- a/lib/prima_auth0_ex/token_provider/auth0_authorization_service.ex +++ b/lib/prima_auth0_ex/token_provider/auth0_authorization_service.ex @@ -6,14 +6,13 @@ defmodule PrimaAuth0Ex.TokenProvider.Auth0AuthorizationService do @behaviour PrimaAuth0Ex.TokenProvider.AuthorizationService require Logger + alias PrimaAuth0Ex.OpenIDConfiguration alias PrimaAuth0Ex.TokenProvider.TokenInfo - @auth0_token_api_path "/oauth/token" - @impl PrimaAuth0Ex.TokenProvider.AuthorizationService def retrieve_token(credentials, audience) do + url = OpenIDConfiguration.fetch!(credentials.base_url).token_endpoint request_body = body(credentials, audience) - url = credentials.base_url |> URI.merge(@auth0_token_api_path) |> URI.to_string() Logger.info("Requesting token to Auth0", client: credentials.client, diff --git a/localauth0.toml b/localauth0.toml new file mode 100644 index 00000000..ee357a37 --- /dev/null +++ b/localauth0.toml @@ -0,0 +1,5 @@ +issuer = "https://your-auth0-tenant.com" + +[[audience]] +name = "server" +permissions = [ "1st:perm", "2nd:perm", "some:permission"] diff --git a/test/integration/plug/verify_and_validate_token_test.exs b/test/integration/plug/verify_and_validate_token_test.exs index f1d57f30..1379e1ec 100644 --- a/test/integration/plug/verify_and_validate_token_test.exs +++ b/test/integration/plug/verify_and_validate_token_test.exs @@ -22,7 +22,7 @@ defmodule PrimaAuth0Ex.Plug.VerifyAndValidateTokenTest do :get |> conn("/") |> put_req_header("authorization", "Bearer " <> token.jwt) - |> VerifyAndValidateToken.call(VerifyAndValidateToken.init([])) + |> VerifyAndValidateToken.call(VerifyAndValidateToken.init(required_permissions: ["1st:perm", "some:permission"])) refute conn.status == 401 end diff --git a/test/prima_auth0_ex/config_test.exs b/test/prima_auth0_ex/config_test.exs index 1b4b4272..1763b40d 100644 --- a/test/prima_auth0_ex/config_test.exs +++ b/test/prima_auth0_ex/config_test.exs @@ -7,18 +7,18 @@ defmodule ConfigTest do test "by getting whole config" do config = Config.default_client() - assert config[:auth0_base_url] == "default" - assert config[:client_id] == "default" - assert config[:client_secret] == "default" + assert config[:auth0_base_url] == "http://localauth0:3000" + assert config[:client_id] == "client_id" + assert config[:client_secret] == "client_secret" assert config[:cache_namespace] == "default" assert config[:token_check_interval] == :timer.seconds(1) assert config[:signature_check_interval] == :timer.seconds(1) end test "by getting specific props" do - assert Config.default_client(:auth0_base_url) == "default" - assert Config.default_client(:client_id) == "default" - assert Config.default_client(:client_secret) == "default" + assert Config.default_client(:auth0_base_url) == "http://localauth0:3000" + assert Config.default_client(:client_id) == "client_id" + assert Config.default_client(:client_secret) == "client_secret" assert Config.default_client(:cache_namespace) == "default" assert Config.default_client(:token_check_interval) == :timer.seconds(1) assert Config.default_client(:signature_check_interval) == :timer.seconds(1) @@ -31,7 +31,7 @@ defmodule ConfigTest do test "bang version" do assert_raise KeyError, fn -> Config.default_client!(:non_existing_property) end - assert Config.default_client!(:client_id) == "default" + assert Config.default_client!(:client_id) == "client_id" end end @@ -81,23 +81,23 @@ defmodule ConfigTest do test "by getting whole config" do config = Config.server() - assert config[:auth0_base_url] == "server" + assert config[:auth0_base_url] == "http://localauth0:3000" assert config[:ignore_signature] == false assert config[:audience] == "server" - assert config[:issuer] == "server" + assert config[:issuer] == "https://your-auth0-tenant.com" assert config[:first_jwks_fetch_sync] == true end test "by getting specific props" do - assert Config.server(:auth0_base_url) == "server" + assert Config.server(:auth0_base_url) == "http://localauth0:3000" assert Config.server(:ignore_signature) == false assert Config.server(:audience) == "server" - assert Config.server(:issuer) == "server" + assert Config.server(:issuer) == "https://your-auth0-tenant.com" assert Config.server(:first_jwks_fetch_sync) == true end test "by getting specific props with a bang" do - assert Config.server!(:auth0_base_url) == "server" + assert Config.server!(:auth0_base_url) == "http://localauth0:3000" assert_raise KeyError, fn -> Config.server!(:non_existing_property) end end end diff --git a/test/prima_auth0_ex/openid_configuration_test.exs b/test/prima_auth0_ex/openid_configuration_test.exs new file mode 100644 index 00000000..98454fbf --- /dev/null +++ b/test/prima_auth0_ex/openid_configuration_test.exs @@ -0,0 +1,44 @@ +defmodule PrimaAuth0Ex.OpenIDConfigurationTest do + use ExUnit.Case, async: true + + alias PrimaAuth0Ex.OpenIDConfiguration + + setup do + bypass = Bypass.open() + {:ok, bypass: bypass} + end + + test "Fetches and parses metadata from a server", %{bypass: bypass} do + config = openid_configuration(bypass) + + Bypass.expect_once(bypass, "GET", "/.well-known/openid-configuration", fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(config)) + end) + + base_url = "http://localhost:#{bypass.port}" + + fetched = OpenIDConfiguration.fetch!(base_url) + + assert fetched.issuer == config.issuer + assert fetched.token_endpoint == config.token_endpoint + assert fetched.jwks_uri == config.jwks_uri + end + + defp openid_configuration(bypass) do + %{ + issuer: "https://tenant.eu.auth0.com/", + authorization_endpoint: "http://localhost:#{bypass.port}/oauth/login", + token_endpoint: "http://localhost:#{bypass.port}/oauth/token", + jwks_uri: "http://localhost:#{bypass.port}/jwks.json", + response_types_supported: [ + "token id_token" + ], + subject_types_supported: [ + "public" + ], + id_token_signing_alg_values_supported: [ + "RS256" + ] + } + end +end diff --git a/test/prima_auth0_ex/token_provider/auth0_authorization_service_test.exs b/test/prima_auth0_ex/token_provider/auth0_authorization_service_test.exs index a706dbb8..75b78aaf 100644 --- a/test/prima_auth0_ex/token_provider/auth0_authorization_service_test.exs +++ b/test/prima_auth0_ex/token_provider/auth0_authorization_service_test.exs @@ -19,6 +19,11 @@ defmodule PrimaAuth0Ex.TokenProvider.Auth0AuthorizationServiceTest do setup do bypass = Bypass.open() + + Bypass.stub(bypass, "GET", "/.well-known/openid-configuration", fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(openid_configuration(bypass))) + end) + {:ok, bypass: bypass} end @@ -114,4 +119,22 @@ defmodule PrimaAuth0Ex.TokenProvider.Auth0AuthorizationServiceTest do end defp valid_auth0_response, do: ~s<{"access_token":"#{sample_token()}","expires_in":86400,"token_type":"Bearer"}> + + defp openid_configuration(bypass) do + %{ + issuer: "https://tenant.eu.auth0.com/", + authorization_endpoint: "http://localhost:#{bypass.port}/oauth/login", + token_endpoint: "http://localhost:#{bypass.port}/oauth/token", + jwks_uri: "http://localhost:#{bypass.port}/jwks.json", + response_types_supported: [ + "token id_token" + ], + subject_types_supported: [ + "public" + ], + id_token_signing_alg_values_supported: [ + "RS256" + ] + } + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index a1e6929d..e077f2ba 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,4 +1,3 @@ -ExUnit.configure(exclude: :external) ExUnit.start(capture_log: true) defmodule PrimaAuth0Ex.TestHelper do