diff --git a/lib/latch.ex b/lib/latch.ex index fdb5104..9ae2033 100644 --- a/lib/latch.ex +++ b/lib/latch.ex @@ -20,7 +20,9 @@ defmodule Latch do def start_link(opts) do name = Keyword.fetch!(opts, :name) - Supervisor.start_link(__MODULE__, opts, name: name) + config = Config.build!(opts) + + Supervisor.start_link(__MODULE__, %{name: name, config: config}, name: name) end def child_spec(opts) do @@ -34,17 +36,13 @@ defmodule Latch do end @impl Supervisor - def init(opts) do - config = Keyword.fetch!(opts, :config) - name = Keyword.fetch!(opts, :name) - config = Map.put(config, :name, name) - + def init(%{name: name, config: config}) do :persistent_term.put({__MODULE__, self()}, config) :persistent_term.put({__MODULE__, name}, config) Supervisor.init( [ - {Latch.NonceCache, opts} + {Latch.NonceCache, config: config, name: name} ], strategy: :one_for_one ) diff --git a/lib/latch/config.ex b/lib/latch/config.ex index b0e5dc4..ab3e65e 100644 --- a/lib/latch/config.ex +++ b/lib/latch/config.ex @@ -1,6 +1,6 @@ defmodule Latch.Config do @moduledoc """ - The configuration that drives the client. You can create many of these. + The configuration that drives the client. ## Fields * `:store` - a module implementing `Latch.Store` @@ -10,12 +10,13 @@ defmodule Latch.Config do * `:signing_key` - the ES256 `JOSE.JWK` private key for `private_key_jwt` * `:client_name` - shown on the authorization consent screen * `:client_uri` - client home page + * `:name` - the name of the Latch instance """ @default_request_ttl 600 - @enforce_keys [:store, :client_id, :redirect_uri, :scope, :signing_key] - defstruct @enforce_keys ++ [:client_name, :client_uri, :name, request_ttl: @default_request_ttl] + @enforce_keys [:store, :client_id, :redirect_uri, :scope, :signing_key, :name] + defstruct @enforce_keys ++ [:client_name, :client_uri, request_ttl: @default_request_ttl] @type t :: %__MODULE__{ store: module(), @@ -23,8 +24,39 @@ defmodule Latch.Config do redirect_uri: String.t(), scope: String.t(), signing_key: JOSE.JWK.t(), + name: atom() | pid(), client_name: String.t() | nil, client_uri: String.t() | nil, - name: term() + request_ttl: pos_integer() } + + @schema [ + store: [type: :atom, required: true], + client_id: [type: :string, required: true], + redirect_uri: [type: :string, required: true], + scope: [type: :string, required: true], + signing_key: [type: :any, required: true], + name: [type: {:or, [:atom, :pid]}, required: true], + client_name: [type: :string, required: false], + client_uri: [type: :string, required: false], + request_ttl: [type: :pos_integer, required: false, default: @default_request_ttl] + ] + + @doc false + def build!(opts) when is_list(opts) do + validated = NimbleOptions.validate!(opts, @schema) + + struct!( + __MODULE__, + store: validated[:store], + client_id: validated[:client_id], + redirect_uri: validated[:redirect_uri], + scope: validated[:scope], + signing_key: validated[:signing_key], + name: validated[:name], + client_name: validated[:client_name], + client_uri: validated[:client_uri], + request_ttl: validated[:request_ttl] + ) + end end diff --git a/mix.exs b/mix.exs index 3af1cc2..9f44f9a 100644 --- a/mix.exs +++ b/mix.exs @@ -37,7 +37,8 @@ defmodule Latch.MixProject do {:jose, "~> 1.11"}, {:jason, "~> 1.2"}, {:credo, "~> 1.7", only: [:dev, :test]}, - {:mimic, "~> 2.3", only: :test} + {:mimic, "~> 2.3", only: :test}, + {:nimble_options, "~> 1.1"} ] end diff --git a/mix.lock b/mix.lock index 8908305..b8552f6 100644 --- a/mix.lock +++ b/mix.lock @@ -9,7 +9,7 @@ "jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mimic": {:hex, :mimic, "2.3.0", "88b1d13c285e57df6ea57204317bb56e49e7329668006cdcb80a9aafc73a9616", [:mix], [{:ham, "~> 0.3", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "52771f23689398c5d41c7d05e91c2c28e10df273b784f40ca8b02e35e46850d3"}, - "mint": {:hex, :mint, "1.9.1", "3bc120b743ed2e99ad920910f2613e9faebabb2257731b0e2ea4d8ccd9eceede", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "831101bd560b086316fab5f7adb21a4f3455717d8e4bc8368b052e09aa9163e0"}, + "mint": {:hex, :mint, "1.9.2", "6e89e698d69cc29be001afd02c2cf4bae2d0994efe69a2ced7aab440d71584f9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d8e952b432fdac2321f570d29a68b9eb2664dc80a7a2ef9464531a5425abf222"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "req": {:hex, :req, "0.6.2", "b9b2024f35bcf60a92cc8cad2eaaf9d4e7aace463ff74be1afe5986830184413", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cc9cd30a2ddd04989929b887178e1610c940456d962c6c3a52df6146d2eef9bf"}, diff --git a/test/latch/client_test.exs b/test/latch/client_test.exs index 2975298..c475b81 100644 --- a/test/latch/client_test.exs +++ b/test/latch/client_test.exs @@ -24,7 +24,8 @@ defmodule Latch.ClientTest do client_id: @client_id, redirect_uri: @redirect_uri, scope: "atproto", - signing_key: nil + signing_key: nil, + name: :name } stale_session = session("stale-access-token", ~U[2020-01-01 00:00:00Z]) @@ -60,7 +61,8 @@ defmodule Latch.ClientTest do client_id: @client_id, redirect_uri: @redirect_uri, scope: "atproto", - signing_key: nil + signing_key: nil, + name: :name } stale_session = session("stale-access-token", ~U[2030-01-01 00:00:00Z]) diff --git a/test/latch/config_test.exs b/test/latch/config_test.exs new file mode 100644 index 0000000..f4ead4f --- /dev/null +++ b/test/latch/config_test.exs @@ -0,0 +1,58 @@ +defmodule Latch.ConfigTest do + use ExUnit.Case, async: true + + alias Latch.Config + + @store Latch.TestStore + @client_id "client_id" + @redirect_uri "redirect_uri" + @scope "atproto something" + @signing_key "signing_key" + @name :name + @client_name "client_name" + @client_uri "client_uri" + + describe "build!/1" do + test "happy path" do + assert %Config{} = config = Config.build!(opts([])) + + assert config.store == @store + assert config.client_id == @client_id + assert config.redirect_uri == @redirect_uri + assert config.scope == @scope + assert config.signing_key == @signing_key + assert config.name == @name + assert config.client_name == @client_name + assert config.client_uri == @client_uri + + # Verify that the default is populated + assert config.request_ttl + end + + test "fails on invalid" do + assert_raise NimbleOptions.ValidationError, fn -> + Config.build!(opts(store: "string")) + end + + assert_raise NimbleOptions.ValidationError, fn -> + Config.build!(opts(doesntexist: "string")) + end + end + end + + defp opts(overrides) do + Keyword.merge( + [ + store: @store, + client_id: @client_id, + redirect_uri: @redirect_uri, + scope: @scope, + signing_key: @signing_key, + name: @name, + client_name: @client_name, + client_uri: @client_uri + ], + overrides + ) + end +end diff --git a/test/latch_test.exs b/test/latch_test.exs index 535a7c3..daea220 100644 --- a/test/latch_test.exs +++ b/test/latch_test.exs @@ -3,7 +3,6 @@ defmodule LatchTest do use Mimic doctest Latch - alias Latch.Config alias Latch.Discovery alias Latch.Flow alias Latch.Identity @@ -22,25 +21,25 @@ defmodule LatchTest do test "resolves identity, creates PAR, stores the request, and returns the redirect URL" do request_uri = "urn:ietf:params:oauth:request_uri:request" - config = %Config{ - store: Latch.TestStore, - client_id: @client_id, - redirect_uri: @redirect_uri, - scope: "atproto", - signing_key: nil - } + pid = + start_latch( + store: Latch.TestStore, + client_id: @client_id, + redirect_uri: @redirect_uri, + scope: "atproto", + signing_key: nil + ) - pid = start_latch(config) identity = %Identity{did: @did, handle: @handle, pds_endpoint: @pds} server = server() expect(Identity, :resolve_handle, fn @handle -> {:ok, identity} end) expect(Discovery, :discover, fn @pds -> {:ok, server} end) - expect(Flow, :par, fn _config, ^server, opts -> + expect(Flow, :par, fn config, ^server, opts -> assert opts[:client_id] == config.client_id assert opts[:redirect_uri] == config.redirect_uri - assert opts[:scope] == "atproto" + assert opts[:scope] == config.scope assert opts[:login_hint] == @handle assert is_binary(opts[:state]) assert is_binary(opts[:code_challenge]) @@ -82,15 +81,14 @@ defmodule LatchTest do code = "authorization-code" dpop_key = JOSE.JWK.generate_key({:ec, "P-256"}) - config = %Config{ - store: Latch.TestStore, - client_id: @client_id, - redirect_uri: @redirect_uri, - scope: "atproto", - signing_key: nil - } - - pid = start_latch(config) + pid = + start_latch( + store: Latch.TestStore, + client_id: @client_id, + redirect_uri: @redirect_uri, + scope: "atproto", + signing_key: nil + ) request = %Request{ state: state, @@ -144,15 +142,14 @@ defmodule LatchTest do describe "refresh/2" do test "rediscovers the authorization server and refreshes the session" do - config = %Config{ - store: Latch.TestStore, - client_id: @client_id, - redirect_uri: @redirect_uri, - scope: "atproto", - signing_key: nil - } - - pid = start_latch(config) + pid = + start_latch( + store: Latch.TestStore, + client_id: @client_id, + redirect_uri: @redirect_uri, + scope: "atproto", + signing_key: nil + ) session = %Session{ did: @did, @@ -170,9 +167,9 @@ defmodule LatchTest do expect(Discovery, :discover, fn @pds -> {:ok, server} end) - expect(Flow, :refresh, fn _config, ^server, ^session, opts -> + expect(Flow, :refresh, fn config, ^server, ^session, opts -> assert opts[:client_id] == config.client_id - assert opts[:client_jwk] == config.signing_key + assert opts[:client_jwk] == nil {:ok, refreshed_session} end) @@ -190,8 +187,9 @@ defmodule LatchTest do } end - defp start_latch(config) do + defp start_latch(opts) do name = String.to_atom("latch_#{inspect(self())}") - start_link_supervised!({Latch, name: name, config: config}) + opts = Keyword.put(opts, :name, name) + start_link_supervised!({Latch, opts}) end end