diff --git a/lib/latch/client_assertion.ex b/lib/latch/client_assertion.ex index 693a69c..0689f57 100644 --- a/lib/latch/client_assertion.ex +++ b/lib/latch/client_assertion.ex @@ -33,9 +33,10 @@ defmodule Latch.ClientAssertion do `exp` is `iat` + #{@lifetime_seconds}s: atproto does not require it, but RFC 7523 does, and servers expect assertions younger than a minute. """ + @spec sign(map(), String.t(), String.t(), keyword()) :: String.t() + def sign(key_map, client_id, audience, opts \\ []) do + jwk = JOSE.JWK.from(key_map) - @spec sign(JOSE.JWK.t(), String.t(), String.t(), keyword()) :: String.t() - def sign(jwk, client_id, audience, opts \\ []) do jti = Keyword.get(opts, :jti, random_b64(20)) iat = Keyword.get(opts, :iat, System.os_time(:second)) @@ -67,8 +68,9 @@ defmodule Latch.ClientAssertion do The published client metadata JWKs must use the same value so the authorization server can match assertion headers to a key. """ - @spec kid(JOSE.JWK.t()) :: String.t() - def kid(jwk) do + @spec kid(map()) :: String.t() + def kid(key_map) do + jwk = JOSE.JWK.from(key_map) {_, map} = JOSE.JWK.to_map(jwk) Map.get_lazy(map, "kid", fn -> JOSE.JWK.thumbprint(jwk) end) end diff --git a/lib/latch/client_metadata.ex b/lib/latch/client_metadata.ex index 814fd36..0c58f44 100644 --- a/lib/latch/client_metadata.ex +++ b/lib/latch/client_metadata.ex @@ -51,9 +51,10 @@ defmodule Latch.ClientMetadata do |> maybe_put("client_uri", client_uri) end - defp public_jwk(jwk) do + defp public_jwk(key_map) do + jwk = JOSE.JWK.from(key_map) {_, public} = JOSE.JWK.to_public_map(jwk) - Map.put(public, "kid", ClientAssertion.kid(jwk)) + Map.put(public, "kid", ClientAssertion.kid(key_map)) end defp maybe_put(map, _key, nil), do: map diff --git a/lib/latch/config.ex b/lib/latch/config.ex index ab3e65e..c447633 100644 --- a/lib/latch/config.ex +++ b/lib/latch/config.ex @@ -23,7 +23,7 @@ defmodule Latch.Config do client_id: String.t(), redirect_uri: String.t(), scope: String.t(), - signing_key: JOSE.JWK.t(), + signing_key: map(), name: atom() | pid(), client_name: String.t() | nil, client_uri: String.t() | nil, diff --git a/lib/latch/dpop.ex b/lib/latch/dpop.ex index 8c47de7..cbe8ab9 100644 --- a/lib/latch/dpop.ex +++ b/lib/latch/dpop.ex @@ -14,7 +14,9 @@ defmodule Latch.DPoP do """ @spec generate_key() :: JOSE.JWK.t() def generate_key do - JOSE.JWK.generate_key({:ec, @curve}) + key = JOSE.JWK.generate_key({:ec, @curve}) + {_, map} = JOSE.JWK.to_map(key) + map end @doc """ @@ -30,8 +32,10 @@ defmodule Latch.DPoP do - `:iat` — override `iat` (tests) Note: atproto currently says **do not** include `iss` on PDS-bound proofs. """ - @spec proof(JOSE.JWK.t(), String.t(), String.t(), keyword()) :: String.t() - def proof(jwk, method, url, opts \\ []) do + @spec proof(map(), String.t(), String.t(), keyword()) :: String.t() + def proof(key_map, method, url, opts \\ []) do + jwk = JOSE.JWK.from(key_map) + jti = Keyword.get(opts, :jti, random_b64(20)) iat = Keyword.get(opts, :iat, System.os_time(:second)) nonce = Keyword.get(opts, :nonce) @@ -70,22 +74,13 @@ defmodule Latch.DPoP do end @doc """ - Serializes a JWK to a JSON string for storage and whatnot. - """ - @spec dump(JOSE.JWK.t()) :: String.t() - def dump(jwk) do - {_, map} = JOSE.JWK.to_map(jwk) - Jason.encode!(map) - end - - @doc """ - Deserializes a JWK from a JSON string. + RFC 7638 thumbprint of a plain JWK map. """ - @spec load(String.t()) :: JOSE.JWK.t() - def load(json) do - json - |> Jason.decode!() + @spec thumbprint(map()) :: String.t() + def thumbprint(key_map) do + key_map |> JOSE.JWK.from() + |> JOSE.JWK.thumbprint() end defp htu(url) do diff --git a/lib/latch/flow.ex b/lib/latch/flow.ex index d826e13..7474c4f 100644 --- a/lib/latch/flow.ex +++ b/lib/latch/flow.ex @@ -197,7 +197,7 @@ defmodule Latch.Flow do defp dpop_request(config, url, build_form, dpop_key) do origin = origin(url) - thumbprint = JOSE.JWK.thumbprint(dpop_key) + thumbprint = DPoP.thumbprint(dpop_key) nonce = case Latch.NonceCache.get_nonce(config, thumbprint, origin) do diff --git a/lib/latch/request.ex b/lib/latch/request.ex index 3d1ae58..a7318b5 100644 --- a/lib/latch/request.ex +++ b/lib/latch/request.ex @@ -1,6 +1,12 @@ defmodule Latch.Request do - @moduledoc false + @moduledoc """ + An in-flight atproto OAuth authorization request, keyed by `state`. + Latch stores this via `Latch.Store` at the start of a login + (`authorize/2`) and consumes it, single use, in `callback/2`. + """ + + @derive {Inspect, except: [:dpop_key]} @enforce_keys [ :state, :did, @@ -21,6 +27,6 @@ defmodule Latch.Request do issuer: String.t(), token_endpoint: String.t(), pkce_verifier: String.t(), - dpop_key: JOSE.JWK.t() + dpop_key: map() } end diff --git a/lib/latch/session.ex b/lib/latch/session.ex index 4bb72bd..79f36f2 100644 --- a/lib/latch/session.ex +++ b/lib/latch/session.ex @@ -9,6 +9,7 @@ defmodule Latch.Session do authenticated XPRC calls go. """ + @derive {Inspect, except: [:access_token, :refresh_token, :dpop_key]} @enforce_keys [ :did, :access_token, @@ -25,7 +26,7 @@ defmodule Latch.Session do did: String.t(), access_token: String.t(), refresh_token: String.t(), - dpop_key: JOSE.JWK.t(), + dpop_key: map(), scope: String.t(), issuer: String.t(), pds_endpoint: String.t(), diff --git a/lib/latch/xrpc.ex b/lib/latch/xrpc.ex index 7f6727e..102f7b3 100644 --- a/lib/latch/xrpc.ex +++ b/lib/latch/xrpc.ex @@ -60,7 +60,7 @@ defmodule Latch.XRPC do defp request(%Config{} = config, %Session{} = session, http_method, url, body) do origin = origin(url) - thumbprint = JOSE.JWK.thumbprint(session.dpop_key) + thumbprint = DPoP.thumbprint(session.dpop_key) nonce = case Latch.NonceCache.get_nonce(config, thumbprint, origin) do diff --git a/test/latch/flow_test.exs b/test/latch/flow_test.exs index a6f1b8a..503f55c 100644 --- a/test/latch/flow_test.exs +++ b/test/latch/flow_test.exs @@ -2,6 +2,7 @@ defmodule Latch.FlowTest do use ExUnit.Case, async: true use Mimic + alias Latch.DPoP alias Latch.Error.SecurityViolation alias Latch.Flow alias Latch.HTTP @@ -27,8 +28,8 @@ defmodule Latch.FlowTest do {Latch.NonceCache, config: config, name: config.name, sweep_disabled: true} ) - client_jwk = JOSE.JWK.generate_key({:ec, "P-256"}) - dpop_key = JOSE.JWK.generate_key({:ec, "P-256"}) + client_jwk = DPoP.generate_key() + dpop_key = DPoP.generate_key() expect(HTTP, :post_form, fn url, form, headers -> assert url == "https://issuer.example.com/oauth/token" @@ -80,8 +81,8 @@ defmodule Latch.FlowTest do describe "par/2" do test "creates a pushed authorization request" do - client_jwk = JOSE.JWK.generate_key({:ec, "P-256"}) - dpop_key = JOSE.JWK.generate_key({:ec, "P-256"}) + client_jwk = DPoP.generate_key() + dpop_key = DPoP.generate_key() config = %Latch.Config{ store: Latch.TestStore, diff --git a/test/latch_test.exs b/test/latch_test.exs index 479aec5..3d8670a 100644 --- a/test/latch_test.exs +++ b/test/latch_test.exs @@ -5,6 +5,7 @@ defmodule LatchTest do alias Latch.Client alias Latch.Discovery + alias Latch.DPoP alias Latch.Flow alias Latch.Identity alias Latch.Request @@ -44,7 +45,7 @@ defmodule LatchTest do assert opts[:login_hint] == @handle assert is_binary(opts[:state]) assert is_binary(opts[:code_challenge]) - assert %JOSE.JWK{} = opts[:dpop_key] + assert %{} = opts[:dpop_key] {:ok, request_uri} end) @@ -72,7 +73,7 @@ defmodule LatchTest do assert request.issuer == @issuer assert request.token_endpoint == @issuer <> "/oauth/token" assert is_binary(request.pkce_verifier) - assert %JOSE.JWK{} = request.dpop_key + assert %{} = request.dpop_key end end @@ -80,7 +81,7 @@ defmodule LatchTest do test "consumes the request, verifies the issuer, and exchanges the authorization code" do state = "state-123" code = "authorization-code" - dpop_key = JOSE.JWK.generate_key({:ec, "P-256"}) + dpop_key = DPoP.generate_key() pid = start_latch(