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-1195]: rfc8414 compliance #185

Merged
merged 22 commits into from
Nov 21, 2023
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
34 changes: 28 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

---

Expand Down
7 changes: 0 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ services:
stdin_open: true
depends_on:
- redis
- localauth0

redis:
image: public.ecr.aws/bitnami/redis:5.0
Expand All @@ -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:
8 changes: 3 additions & 5 deletions lib/prima_auth0_ex/jwks_strategy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 40 additions & 0 deletions lib/prima_auth0_ex/openid_configuration.ex
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion lib/prima_auth0_ex/token_cache/token_cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions localauth0.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
issuer = "https://your-auth0-tenant.com"

[[audience]]
name = "server"
permissions = [ "1st:perm", "2nd:perm", "some:permission"]
2 changes: 1 addition & 1 deletion test/integration/plug/verify_and_validate_token_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions test/prima_auth0_ex/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions test/prima_auth0_ex/openid_configuration_test.exs
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading