diff --git a/README.md b/README.md index 31b454a..531d875 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,29 @@ client_secret: System.get_env("AUTH0_CLIENT_SECRET") ``` + **or** with computed configurations: + + ```elixir + defmodule MyApp.ConfigFrom do + def get_domain(%Plug.Conn{} = conn) do + ... + end + + def get_client_id(%Plug.Conn{} = conn) do + ... + end + + def get_client_secret(%Plug.Conn{} = conn) do + ... + end + end + ``` + + ```elixir + config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, + config_from: MyApp.ConfigFrom + ``` + See the `Ueberauth.Strategy.Auth0` module docs for more configuration options. diff --git a/config/config.exs b/config/config.exs index 69c67b3..d51deeb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,6 @@ # This file is responsible for configuring your application -# and its dependencies with the aid of the Mix.Config module. -use Mix.Config +# and its dependencies with the aid of the Config module. +import Config # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this @@ -27,5 +27,5 @@ use Mix.Config # Configuration from the imported file will override the ones defined # here (which is why it is important to import them last). # -# import_config "#{Mix.env}.exs" -if Mix.env() == :test, do: import_config("test.exs") +# import_config "#{config_env()}.exs" +if config_env() == :test, do: import_config("test.exs") diff --git a/config/test.exs b/config/test.exs index da155c7..3e4ee5c 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :ueberauth, Ueberauth, json_library: Jason, @@ -8,8 +8,8 @@ config :ueberauth, Ueberauth, config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, domain: "example-app.auth0.com", - client_id: "clientidsomethingrandom", - client_secret: "clientsecret-somethingsecret" + client_id: "example-client-id", + client_secret: "example-client-secret" config :exvcr, vcr_cassette_library_dir: "test/fixtures/vcr_cassettes" diff --git a/lib/ueberauth/strategy/auth0.ex b/lib/ueberauth/strategy/auth0.ex index 453bd6a..77aa79a 100644 --- a/lib/ueberauth/strategy/auth0.ex +++ b/lib/ueberauth/strategy/auth0.ex @@ -118,7 +118,7 @@ defmodule Ueberauth.Strategy.Auth0 do module = option(conn, :oauth2_module) - callback_url = module.authorize_url!(opts, [otp_app: option(conn, :otp_app)]) + callback_url = module.authorize_url!(conn, opts, otp_app: option(conn, :otp_app)) redirect!(conn, callback_url) end @@ -132,7 +132,10 @@ defmodule Ueberauth.Strategy.Auth0 do module = option(conn, :oauth2_module) redirect_uri = callback_url(conn) - result = module.get_token!([code: code, redirect_uri: redirect_uri], [otp_app: option(conn, :otp_app)]) + result = + module.get_token!(conn, [code: code, redirect_uri: redirect_uri], + otp_app: option(conn, :otp_app) + ) case result do {:ok, client} -> diff --git a/lib/ueberauth/strategy/auth0/oauth.ex b/lib/ueberauth/strategy/auth0/oauth.ex index a02918c..d27d143 100644 --- a/lib/ueberauth/strategy/auth0/oauth.ex +++ b/lib/ueberauth/strategy/auth0/oauth.ex @@ -13,6 +13,25 @@ defmodule Ueberauth.Strategy.Auth0.OAuth do client_id: {:system, "AUTH0_CLIENT_ID"}, client_secret: {:system, "AUTH0_CLIENT_SECRET"} + Or using a computed configuration: + + defmodule MyApp.ConfigFrom do + def get_domain(%Plug.Conn{} = conn) do + ... + end + + def get_client_id(%Plug.Conn{} = conn) do + ... + end + + def get_client_secret(%Plug.Conn{} = conn) do + ... + end + end + + config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, + config_from: MyApp.ConfigFrom + The JSON serializer used is the same as `Ueberauth` so if you need to customize it, you can configure it in the `Ueberauth` configuration: @@ -24,8 +43,9 @@ defmodule Ueberauth.Strategy.Auth0.OAuth do alias OAuth2.Client alias OAuth2.Strategy.AuthCode - def options(otp_app) do + def options(conn, otp_app) do configs = Application.get_env(otp_app || :ueberauth, Ueberauth.Strategy.Auth0.OAuth) + configs = compute_configs(conn, configs) unless configs do raise( @@ -63,10 +83,10 @@ defmodule Ueberauth.Strategy.Auth0.OAuth do This will be setup automatically for you in `Ueberauth.Strategy.Auth0`. These options are only useful for usage outside the normal callback phase of Ueberauth. """ - def client(opts \\ []) do - opts - |> Keyword.get(:otp_app) - |> options() + def client(conn, opts \\ []) do + otp_app = Keyword.get(opts, :otp_app) + + options(conn, otp_app) |> Keyword.merge(opts) |> Client.new() end @@ -74,18 +94,16 @@ defmodule Ueberauth.Strategy.Auth0.OAuth do @doc """ Provides the authorize url for the request phase of Ueberauth. No need to call this usually. """ - def authorize_url!(params \\ [], opts \\ []) do - opts - |> client + def authorize_url!(conn, params \\ [], opts \\ []) do + client(conn, opts) |> Client.authorize_url!(params) end - def get_token!(params \\ [], opts \\ []) do + def get_token!(conn, params \\ [], opts \\ []) do otp_app = Keyword.get(opts, :otp_app) client_secret = - otp_app - |> options() + options(conn, otp_app) |> Keyword.get(:client_secret) params = Keyword.merge(params, client_secret: client_secret) @@ -97,7 +115,7 @@ defmodule Ueberauth.Strategy.Auth0.OAuth do |> Keyword.get(:client_options, []) |> Keyword.merge(otp_app: otp_app) - Client.get_token(client(client_options), params, headers, opts) + Client.get_token(client(conn, client_options), params, headers, opts) end # Strategy Callbacks @@ -114,4 +132,39 @@ defmodule Ueberauth.Strategy.Auth0.OAuth do defp get_config_value({:system, value}), do: System.get_env(value) defp get_config_value(value), do: value + + defp compute_configs(conn, configs) do + case conn do + %Plug.Conn{} = conn when not is_nil(configs) -> + with module when not is_nil(module) <- + Keyword.get(configs, :config_from), + {:loaded, {:module, _module}} <- {:loaded, Code.ensure_loaded(module)}, + {:exported, true} <- {:exported, function_exported?(module, :get_domain, 1)}, + {:exported, true} <- {:exported, function_exported?(module, :get_client_id, 1)}, + {:exported, true} <- {:exported, function_exported?(module, :get_client_secret, 1)} do + configs + |> Keyword.merge( + domain: apply(module, :get_domain, [conn]), + client_id: apply(module, :get_client_id, [conn]), + client_secret: apply(module, :get_client_secret, [conn]) + ) + else + {:loaded, {:error, :nofile}} -> + raise("Couldn't load module from `:config_from`") + + {:exported, false} -> + raise( + "When using `:config_from`, the given module should export 3 functions: `get_domain/1`, `get_client_id/1` and `get_client_secret/1`" + ) + + # Used base configuration. + _ -> + configs + end + + # Both configurations weren't found. + _ -> + configs + end + end end diff --git a/mix.exs b/mix.exs index 4d854d2..51eb519 100644 --- a/mix.exs +++ b/mix.exs @@ -32,7 +32,8 @@ defmodule UeberauthAuth0.Mixfile do # Type checking dialyzer: [ plt_core_path: "_build/#{Mix.env()}" - ] + ], + elixirc_paths: elixirc_paths(Mix.env()) ] end @@ -51,8 +52,8 @@ defmodule UeberauthAuth0.Mixfile do {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, # Testing: - {:exvcr, "~> 0.10", only: :test}, - {:excoveralls, "~> 0.11", only: :test}, + {:exvcr, "~> 0.10", only: [:test]}, + {:excoveralls, "~> 0.11", only: [:test]}, # Type checking {:dialyxir, "~> 1.2.0", only: [:dev, :test], runtime: false}, @@ -89,4 +90,7 @@ defmodule UeberauthAuth0.Mixfile do } ] end + + defp elixirc_paths(:test), do: ["lib", "test/support", "test/configs"] + defp elixirc_paths(_), do: ["lib"] end diff --git a/test/configs/bad_config_from.exs b/test/configs/bad_config_from.exs new file mode 100644 index 0000000..73c222c --- /dev/null +++ b/test/configs/bad_config_from.exs @@ -0,0 +1,3 @@ +import Config + +config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, config_from: Ueberauth.Support.BadConfigFrom diff --git a/test/configs/config_from.exs b/test/configs/config_from.exs new file mode 100644 index 0000000..8cf630b --- /dev/null +++ b/test/configs/config_from.exs @@ -0,0 +1,3 @@ +import Config + +config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, config_from: Ueberauth.Support.ConfigFrom diff --git a/test/strategy/auth0/oauth_test.exs b/test/strategy/auth0/oauth_test.exs index f194962..3a52084 100644 --- a/test/strategy/auth0/oauth_test.exs +++ b/test/strategy/auth0/oauth_test.exs @@ -1,17 +1,57 @@ defmodule Ueberauth.Strategy.Auth0.OAuthTest do use ExUnit.Case - import Ueberauth.Strategy.Auth0.OAuth, only: [client: 0, client: 1] + import Ueberauth.Strategy.Auth0.OAuth, only: [client: 1, client: 2] @test_domain "example-app.auth0.com" + @conn %Plug.Conn{} - setup do - {:ok, %{client: client()}} + describe "when default configurations are used" do + setup do + {:ok, %{client: client(@conn)}} + end + + test "creates correct client", %{client: client} do + asserts_client_creation(client) + end + + test "raises when there is no configuration" do + assert_raise(RuntimeError, ~r/^Expected to find settings under.*/, fn -> + client(@conn, otp_app: :unknown_auth0_otp_app) + end) + end + end + + describe "when right custom/computed configurations are used" do + setup do + load_configs("config_from") + {:ok, %{client: client(@conn, conn: @conn)}} + end + + test "creates correct client", %{client: client} do + asserts_client_creation(client) + end end - test "creates correct client", %{client: client} do - assert client.client_id == "clientidsomethingrandom" - assert client.client_secret == "clientsecret-somethingsecret" + describe "when bad custom/computed configurations are used" do + setup do + load_configs("bad_config_from") + end + + test "raises when there is bad ConfigFrom module" do + assert_raise( + RuntimeError, + ~r/^When using `:config_from`, the given module should export 3 functions:*/, + fn -> + client(@conn, conn: @conn) + end + ) + end + end + + defp asserts_client_creation(client) do + assert client.client_id == "example-client-id" + assert client.client_secret == "example-client-secret" assert client.redirect_uri == "" assert client.strategy == Ueberauth.Strategy.Auth0.OAuth assert client.authorize_url == "https://#{@test_domain}/authorize" @@ -19,9 +59,18 @@ defmodule Ueberauth.Strategy.Auth0.OAuthTest do assert client.site == "https://#{@test_domain}" end - test "raises when there is no configuration" do - assert_raise(RuntimeError, ~r/^Expected to find settings under.*/, fn -> - client(otp_app: :unknown_auth0_otp_app) - end) + defp load_configs(filename) do + "test/configs/#{filename}.exs" + |> Config.Reader.read!() + |> Application.put_all_env() + + module = + :ueberauth + |> Application.get_env(Ueberauth.Strategy.Auth0.OAuth) + |> Keyword.get(:config_from) + + {:module, _module} = Code.ensure_loaded(module) + + :ok end end diff --git a/test/strategy/auth0_test.exs b/test/strategy/auth0_test.exs index a11f6c8..4e3d8f7 100644 --- a/test/strategy/auth0_test.exs +++ b/test/strategy/auth0_test.exs @@ -56,8 +56,11 @@ defmodule Ueberauth.Strategy.Auth0Test do assert conn.resp_body =~ ~s|You are being redirected.| assert conn.resp_body =~ ~s|href="https://example-app.auth0.com/authorize?| - assert conn.resp_body =~ ~s|client_id=clientidsomethingrandom| - assert conn.resp_body =~ ~s|redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback| + assert conn.resp_body =~ ~s|client_id=example-client-id| + + assert conn.resp_body =~ + ~s|redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback| + assert conn.resp_body =~ ~s|response_type=code| assert conn.resp_body =~ ~s|scope=openid+profile+email| assert conn.resp_body =~ ~s|state=#{conn.private[:ueberauth_state_param]}| @@ -77,11 +80,14 @@ defmodule Ueberauth.Strategy.Auth0Test do assert conn.resp_body =~ ~s|You are being redirected.| assert conn.resp_body =~ ~s|href="https://example-app.auth0.com/authorize?| - assert conn.resp_body =~ ~s|client_id=clientidsomethingrandom| + assert conn.resp_body =~ ~s|client_id=example-client-id| assert conn.resp_body =~ ~s|connection=facebook| assert conn.resp_body =~ ~s|login_hint=user| assert conn.resp_body =~ ~s|screen_hint=signup| - assert conn.resp_body =~ ~s|redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback| + + assert conn.resp_body =~ + ~s|redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback| + assert conn.resp_body =~ ~s|response_type=code| assert conn.resp_body =~ ~s|scope=profile+address+phone| assert conn.resp_body =~ ~s|state=#{conn.private[:ueberauth_state_param]}| @@ -148,14 +154,14 @@ defmodule Ueberauth.Strategy.Auth0Test do |> Plug.Session.call(@session_options) |> SpecRouter.call(@router) - assert conn.resp_body == "auth0 callback" + assert conn.resp_body == "auth0 callback" auth = conn.assigns.ueberauth_failure assert conn.private[:auth0_state] == nil csrf_attack = %Ueberauth.Failure.Error{ message: "Cross-Site Request Forgery attack", - message_key: "csrf_attack", + message_key: "csrf_attack" } assert auth.provider == :auth0 @@ -290,19 +296,19 @@ defmodule Ueberauth.Strategy.Auth0Test do |> Plug.Session.call(@session_options) |> SpecRouter.call(@router) - assert conn.resp_body == "auth0 callback" + assert conn.resp_body == "auth0 callback" - auth = conn.assigns.ueberauth_auth + auth = conn.assigns.ueberauth_auth - # Same information as default token - assert auth.provider == :auth0 - assert auth.strategy == Ueberauth.Strategy.Auth0 - assert auth.uid == "auth0|lyy5v5utb6n9qfm4ihi3l7pv34po66" - assert conn.private.auth0_state == state + # Same information as default token + assert auth.provider == :auth0 + assert auth.strategy == Ueberauth.Strategy.Auth0 + assert auth.uid == "auth0|lyy5v5utb6n9qfm4ihi3l7pv34po66" + assert conn.private.auth0_state == state - ## Difference here - assert auth.credentials.expires == false - assert auth.credentials.expires_at == nil + ## Difference here + assert auth.credentials.expires == false + assert auth.credentials.expires_at == nil end end @@ -329,18 +335,18 @@ defmodule Ueberauth.Strategy.Auth0Test do |> Plug.Session.call(@session_options) |> SpecRouter.call(@router) - assert conn.resp_body == "auth0 callback" + assert conn.resp_body == "auth0 callback" - auth = conn.assigns.ueberauth_failure + auth = conn.assigns.ueberauth_failure - token_unauthorized = %Ueberauth.Failure.Error{ - message: "unauthorized_token", - message_key: "OAuth2" - } + token_unauthorized = %Ueberauth.Failure.Error{ + message: "unauthorized_token", + message_key: "OAuth2" + } - assert auth.provider == :auth0 - assert auth.strategy == Ueberauth.Strategy.Auth0 - assert auth.errors == [token_unauthorized] + assert auth.provider == :auth0 + assert auth.strategy == Ueberauth.Strategy.Auth0 + assert auth.errors == [token_unauthorized] end end @@ -367,18 +373,18 @@ defmodule Ueberauth.Strategy.Auth0Test do |> Plug.Session.call(@session_options) |> SpecRouter.call(@router) - assert conn.resp_body == "auth0 callback" + assert conn.resp_body == "auth0 callback" - auth = conn.assigns.ueberauth_failure + auth = conn.assigns.ueberauth_failure - some_error_in_body = %Ueberauth.Failure.Error{ - message: %{"error" => "something_wrong", "error_description" => "Something went wrong"}, - message_key: "OAuth2" - } + some_error_in_body = %Ueberauth.Failure.Error{ + message: %{"error" => "something_wrong", "error_description" => "Something went wrong"}, + message_key: "OAuth2" + } - assert auth.provider == :auth0 - assert auth.strategy == Ueberauth.Strategy.Auth0 - assert auth.errors == [some_error_in_body] + assert auth.provider == :auth0 + assert auth.strategy == Ueberauth.Strategy.Auth0 + assert auth.errors == [some_error_in_body] end end diff --git a/test/support/bad_config_from.ex b/test/support/bad_config_from.ex new file mode 100644 index 0000000..554eb2c --- /dev/null +++ b/test/support/bad_config_from.ex @@ -0,0 +1,9 @@ +defmodule Ueberauth.Support.BadConfigFrom do + @moduledoc false + + # def get_client_id(%Plug.Conn{} = _conn), do: "example-client-id" + + # def get_client_secret(%Plug.Conn{} = _conn), do: "example-client-secret" + + # def get_domain(%Plug.Conn{} = _conn), do: "example-app.auth0.com" +end diff --git a/test/support/config_from.ex b/test/support/config_from.ex new file mode 100644 index 0000000..9602e87 --- /dev/null +++ b/test/support/config_from.ex @@ -0,0 +1,9 @@ +defmodule Ueberauth.Support.ConfigFrom do + @moduledoc false + + def get_client_id(%Plug.Conn{} = _conn), do: "example-client-id" + + def get_client_secret(%Plug.Conn{} = _conn), do: "example-client-secret" + + def get_domain(%Plug.Conn{} = _conn), do: "example-app.auth0.com" +end