diff --git a/lib/boruta/adapters/internal/signatures.ex b/lib/boruta/adapters/internal/signatures.ex index 635724d0..68ae3a64 100644 --- a/lib/boruta/adapters/internal/signatures.ex +++ b/lib/boruta/adapters/internal/signatures.ex @@ -1,6 +1,8 @@ defmodule Boruta.Internal.Signatures do @behaviour Boruta.Oauth.Signatures + import Boruta.Config, only: [resource_owners: 0] + defmodule Token do @moduledoc false @@ -161,13 +163,16 @@ defmodule Boruta.Internal.Signatures do do: @signature_algorithms[String.to_atom(signature_alg)][:type] defp get_signing_key(client, :id_token) do - {:ok, - %SigningKey{ - type: :internal, - private_key: client.private_key, - secret: client.secret, - kid: client.did || client.id_token_kid || Client.Crypto.kid_from_private_key(client.private_key) - }} + with {:ok, trust_chain} <- resource_owners().trust_chain(client) do + {:ok, + %SigningKey{ + type: :internal, + private_key: client.private_key, + secret: client.secret, + kid: client.did || client.id_token_kid || Client.Crypto.kid_from_private_key(client.private_key), + trust_chain: trust_chain + }} + end end defp get_signing_key(client, :userinfo) do @@ -181,12 +186,15 @@ defmodule Boruta.Internal.Signatures do end defp get_signing_key(client, :verifiable_credential) do - {:ok, - %SigningKey{ - type: :internal, - private_key: client.private_key, - secret: client.secret, - kid: client.did || client.id_token_kid || Client.Crypto.kid_from_private_key(client.private_key) - }} + with {:ok, trust_chain} <- resource_owners().trust_chain(client) do + {:ok, + %SigningKey{ + type: :internal, + private_key: client.private_key, + secret: client.secret, + kid: client.did || client.id_token_kid || Client.Crypto.kid_from_private_key(client.private_key), + trust_chain: trust_chain + }} + end end end diff --git a/lib/boruta/adapters/universal/signatures.ex b/lib/boruta/adapters/universal/signatures.ex index 579c89c9..1852ef93 100644 --- a/lib/boruta/adapters/universal/signatures.ex +++ b/lib/boruta/adapters/universal/signatures.ex @@ -1,6 +1,8 @@ defmodule Boruta.Universal.Signatures do @behaviour Boruta.Oauth.Signatures + import Boruta.Config, only: [resource_owners: 0] + defmodule Token do @moduledoc false @@ -113,14 +115,17 @@ defmodule Boruta.Universal.Signatures do do: @signature_algorithms[String.to_atom(signature_alg)][:type] defp get_signing_key(client, :id_token) do - {:ok, - %SigningKey{ - type: :internal, - private_key: client.private_key, - public_key: client.public_key, - secret: client.secret, - kid: client.did - }} + with {:ok, trust_chain} <- resource_owners().trust_chain(client) do + {:ok, + %SigningKey{ + type: :internal, + private_key: client.private_key, + public_key: client.public_key, + secret: client.secret, + kid: client.did, + trust_chain: trust_chain + }} + end end defp get_signing_key(client, :userinfo) do @@ -135,12 +140,15 @@ defmodule Boruta.Universal.Signatures do end defp get_signing_key(client, :verifiable_credential) do - {:ok, - %SigningKey{ - type: :universal, - private_key: client.private_key, - public_key: client.public_key, - kid: client.did - }} + with {:ok, trust_chain} <- resource_owners().trust_chain(client) do + {:ok, + %SigningKey{ + type: :universal, + private_key: client.private_key, + public_key: client.public_key, + kid: client.did, + trust_chain: trust_chain + }} + end end end diff --git a/lib/boruta/adapters/universal/signatures/signing_key.ex b/lib/boruta/adapters/universal/signatures/signing_key.ex index faa538ce..7cc6bc32 100644 --- a/lib/boruta/adapters/universal/signatures/signing_key.ex +++ b/lib/boruta/adapters/universal/signatures/signing_key.ex @@ -20,12 +20,13 @@ defmodule Boruta.Universal.Signatures.SigningKey do trust_chain: list(String.t()) | nil } - def encode_and_sign_with_key(%__MODULE__{kid: kid, private_key: key_id}, payload) do + def encode_and_sign_with_key(%__MODULE__{kid: kid, private_key: key_id, trust_chain: trust_chain}, payload) do header = %{ "typ" => "JWT", "alg" => "EdDSA", - "kid" => kid + "kid" => kid, + "trust_chain" => trust_chain } |> Jason.encode!() |> Base.url_encode64(padding: false) diff --git a/lib/boruta/oauth/authorization.ex b/lib/boruta/oauth/authorization.ex index d18796b8..4e65d215 100644 --- a/lib/boruta/oauth/authorization.ex +++ b/lib/boruta/oauth/authorization.ex @@ -238,6 +238,7 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.AuthorizationCodeRequest d alias Boruta.Oauth.AuthorizationCodeRequest alias Boruta.Oauth.AuthorizationSuccess alias Boruta.Oauth.Client + alias Boruta.Oauth.Error alias Boruta.Oauth.IdToken alias Boruta.Oauth.ResourceOwner alias Boruta.Oauth.Scope @@ -315,9 +316,16 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.AuthorizationCodeRequest d {:ok, %{token: access_token}} {_, true} -> - id_token = IdToken.generate(%{token: access_token}, nonce) - - {:ok, %{token: access_token, id_token: id_token}} + case IdToken.generate(%{token: access_token}, nonce) do + {:ok, id_token} -> + {:ok, %{token: access_token, id_token: id_token}} + {:error, error} -> + {:error, %Error{ + status: :internal_server_error, + error: :unknown_error, + error_description: error + }} + end {_, false} -> {:ok, %{token: access_token}} @@ -434,6 +442,7 @@ end defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.PreauthorizationCodeRequest do alias Boruta.Oauth.Client + alias Boruta.Oauth.Error alias Boruta.AccessTokensAdapter alias Boruta.CodesAdapter alias Boruta.Oauth.Authorization @@ -501,9 +510,16 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.PreauthorizationCodeReques {:ok, _code} <- CodesAdapter.revoke(code) do case String.match?(scope, ~r/#{Scope.openid().name}/) do true -> - id_token = IdToken.generate(%{token: access_token}, nonce) - - {:ok, %{preauthorized_token: access_token, id_token: id_token}} + case IdToken.generate(%{token: access_token}, nonce) do + {:ok, id_token} -> + {:ok, %{preauthorized_token: access_token, id_token: id_token}} + {:error, error} -> + {:error, %Error{ + status: :internal_server_error, + error: :unknown_error, + error_description: error + }} + end false -> {:ok, %{preauthorized_token: access_token}} @@ -536,6 +552,7 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.TokenRequest do alias Boruta.AccessTokensAdapter alias Boruta.Oauth.Authorization alias Boruta.Oauth.AuthorizationSuccess + alias Boruta.Oauth.Error alias Boruta.Oauth.IdToken alias Boruta.Oauth.ResourceOwner alias Boruta.Oauth.Scope @@ -612,8 +629,9 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.TokenRequest do inserted_at: DateTime.utc_now() } - id_token = IdToken.generate(%{base_token: base_token}, nonce) - {:ok, %{id_token: id_token}} + with {:ok, id_token} <- IdToken.generate(%{base_token: base_token}, nonce) do + {:ok, %{id_token: id_token}} + end false -> {:ok, %{}} @@ -622,8 +640,16 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.TokenRequest do "id_token", {:ok, tokens} -> case String.match?(scope, ~r/#{Scope.openid().name}/) do true -> - id_token = IdToken.generate(tokens, nonce) - {:ok, Map.put(tokens, :id_token, id_token)} + case IdToken.generate(tokens, nonce) do + {:ok, id_token} -> + {:ok, Map.put(tokens, :id_token, id_token)} + {:error, error} -> + {:error, %Error{ + status: :internal_server_error, + error: :unknown_error, + error_description: error + }} + end false -> {:ok, tokens} @@ -644,6 +670,13 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.TokenRequest do ) do {:ok, Map.put(tokens, :token, access_token)} end + _, {:error, error} -> + {:error, + %Error{ + status: :internal_server_error, + error: :unknown_error, + error_description: "An error occurred during token creation: #{inspect(error)}." + }} end) end end @@ -1139,9 +1172,9 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.HybridRequest do "id_token", {:ok, tokens} -> case String.match?(scope, ~r/#{Scope.openid().name}/) do true -> - id_token = IdToken.generate(tokens, nonce) - - {:ok, Map.put(tokens, :id_token, id_token)} + with {:ok, id_token} <- IdToken.generate(tokens, nonce) do + {:ok, Map.put(tokens, :id_token, id_token)} + end false -> {:ok, tokens} diff --git a/lib/boruta/oauth/contexts/resource_owners.ex b/lib/boruta/oauth/contexts/resource_owners.ex index 9580b701..02f76ec5 100644 --- a/lib/boruta/oauth/contexts/resource_owners.ex +++ b/lib/boruta/oauth/contexts/resource_owners.ex @@ -31,5 +31,11 @@ defmodule Boruta.Oauth.ResourceOwners do @callback claims(resource_owner :: ResourceOwner.t(), scope :: String.t()) :: claims :: Boruta.Oauth.IdToken.claims() + @doc """ + Returns `id_token` trust_chain for the given client. This will be present in the `trust_chain` header of the resulting `id_token` of OpenID Connect flows. + """ + @callback trust_chain(client :: Boruta.Oauth.Client.t()) :: + {:ok, trust_chain :: list(String.t())} | {:error, reason :: String.t()} + @optional_callbacks claims: 2 end diff --git a/lib/boruta/oauth/error.ex b/lib/boruta/oauth/error.ex index f0096311..ae9cc6ab 100644 --- a/lib/boruta/oauth/error.ex +++ b/lib/boruta/oauth/error.ex @@ -14,7 +14,7 @@ defmodule Boruta.Oauth.Error do alias Boruta.Oauth.TokenRequest @type t :: %__MODULE__{ - status: :internal_server_error | :bad_request | :unauthorized, + status: :internal_server_error | :bad_request | :unauthorized | :not_found, error: :invalid_request | :invalid_client @@ -24,6 +24,7 @@ defmodule Boruta.Oauth.Error do | :invalid_code | :invalid_resource_owner | :login_required + | :not_found | :unknown_error, error_description: String.t(), format: :query | :fragment | :json | nil, diff --git a/lib/boruta/oauth/schemas/id_token.ex b/lib/boruta/oauth/schemas/id_token.ex index ff94d276..852b2df4 100644 --- a/lib/boruta/oauth/schemas/id_token.ex +++ b/lib/boruta/oauth/schemas/id_token.ex @@ -40,12 +40,14 @@ defmodule Boruta.Oauth.IdToken do } } - @spec generate(tokens :: tokens(), nonce :: String.t()) :: id_token :: Oauth.Token.t() + @spec generate(tokens :: tokens(), nonce :: String.t()) :: + {:ok, id_token :: Oauth.Token.t()} | {:error, reason :: String.t()} def generate(tokens, nonce) do {base_token, payload} = payload(tokens, nonce, %{}) - value = Client.Crypto.id_token_sign(payload, base_token.client) - %{base_token | type: "id_token", value: value} + with "" <> value <- Client.Crypto.id_token_sign(payload, base_token.client) do + {:ok, %{base_token | type: "id_token", value: value}} + end end defp payload(%{code: code} = tokens, nonce, acc) do diff --git a/test/boruta/oauth/integration/authorization_code_grant_test.exs b/test/boruta/oauth/integration/authorization_code_grant_test.exs index 6382794e..61d59e4f 100644 --- a/test/boruta/oauth/integration/authorization_code_grant_test.exs +++ b/test/boruta/oauth/integration/authorization_code_grant_test.exs @@ -919,6 +919,9 @@ defmodule Boruta.OauthTest.AuthorizationCodeGrantTest do end test "returns a code with siopv2 (direct_post)" do + stub(Boruta.Support.ResourceOwners, :trust_chain, fn _client -> + {:ok, []} + end) redirect_uri = "openid:" assert {:authorize_success, @@ -990,6 +993,9 @@ defmodule Boruta.OauthTest.AuthorizationCodeGrantTest do end test "returns a code with verifiable presentation (direct_post)" do + stub(Boruta.Support.ResourceOwners, :trust_chain, fn _client -> + {:ok, []} + end) redirect_uri = "openid:" insert(:scope, name: "vp_token", public: true) @@ -1748,6 +1754,7 @@ defmodule Boruta.OauthTest.AuthorizationCodeGrantTest do }} end) |> expect(:claims, fn _sub, _scope -> %{} end) + |> expect(:trust_chain, fn _client -> {:ok, []} end) redirect_uri = List.first(client.redirect_uris) diff --git a/test/boruta/oauth/integration/hybrid_test.exs b/test/boruta/oauth/integration/hybrid_test.exs index bdfec8b2..243cf9b8 100644 --- a/test/boruta/oauth/integration/hybrid_test.exs +++ b/test/boruta/oauth/integration/hybrid_test.exs @@ -388,7 +388,8 @@ defmodule Boruta.OauthTest.HybridGrantTest do test "returns a code and an id_token", %{client: client, resource_owner: resource_owner} do ResourceOwners |> expect(:authorized_scopes, fn _resource_owner -> [] end) - |> expect(:claims, fn _sub, _scope -> %{"email" => resource_owner.username} end) + |> expect(:claims, fn (_sub, _scope) -> %{"email" => resource_owner.username} end) + |> expect(:trust_chain, fn _client -> {:ok, []} end) redirect_uri = List.first(client.redirect_uris) nonce = "nonce" @@ -446,7 +447,8 @@ defmodule Boruta.OauthTest.HybridGrantTest do } do ResourceOwners |> expect(:authorized_scopes, fn _resource_owner -> [] end) - |> expect(:claims, fn _sub, _scope -> %{"email" => resource_owner.username} end) + |> expect(:claims, fn (_sub, _scope) -> %{"email" => resource_owner.username} end) + |> expect(:trust_chain, fn _client -> {:ok, []} end) redirect_uri = List.first(client.redirect_uris) nonce = "nonce" @@ -580,7 +582,8 @@ defmodule Boruta.OauthTest.HybridGrantTest do } do ResourceOwners |> expect(:authorized_scopes, fn _resource_owner -> [] end) - |> expect(:claims, fn _sub, _scope -> %{"email" => resource_owner.username} end) + |> expect(:claims, fn (_sub, _scope) -> %{"email" => resource_owner.username} end) + |> expect(:trust_chain, fn _client -> {:ok, []} end) redirect_uri = List.first(client.redirect_uris) nonce = "nonce" @@ -639,7 +642,8 @@ defmodule Boruta.OauthTest.HybridGrantTest do } do ResourceOwners |> expect(:authorized_scopes, fn _resource_owner -> [] end) - |> expect(:claims, fn _sub, _scope -> %{"email" => resource_owner.username} end) + |> expect(:claims, fn (_sub, _scope) -> %{"email" => resource_owner.username} end) + |> expect(:trust_chain, fn _client -> {:ok, []} end) redirect_uri = List.first(client.redirect_uris) nonce = "nonce" @@ -699,7 +703,8 @@ defmodule Boruta.OauthTest.HybridGrantTest do } do ResourceOwners |> expect(:authorized_scopes, fn _resource_owner -> [] end) - |> expect(:claims, fn _sub, _scope -> %{"email" => resource_owner.username} end) + |> expect(:claims, fn (_sub, _scope) -> %{"email" => resource_owner.username} end) + |> expect(:trust_chain, fn _client -> {:ok, []} end) redirect_uri = "https://wildcard-redirect-uri.uri" nonce = "nonce" diff --git a/test/boruta/oauth/integration/implicit_grant_test.exs b/test/boruta/oauth/integration/implicit_grant_test.exs index a6eb67cb..0c494bf8 100644 --- a/test/boruta/oauth/integration/implicit_grant_test.exs +++ b/test/boruta/oauth/integration/implicit_grant_test.exs @@ -387,6 +387,7 @@ defmodule Boruta.OauthTest.ImplicitGrantTest do ResourceOwners |> expect(:authorized_scopes, fn _resource_owner -> [] end) |> expect(:claims, fn _sub, _scope -> %{} end) + |> expect(:trust_chain, fn _client -> {:ok, []} end) redirect_uri = List.first(client.redirect_uris) nonce = "nonce" @@ -471,6 +472,7 @@ defmodule Boruta.OauthTest.ImplicitGrantTest do ResourceOwners |> expect(:authorized_scopes, fn _resource_owner -> [] end) |> expect(:claims, fn _sub, _scope -> %{} end) + |> expect(:trust_chain, fn _client -> {:ok, []} end) redirect_uri = List.first(client.redirect_uris) nonce = "nonce" diff --git a/test/boruta/oauth/schemas/id_token_test.exs b/test/boruta/oauth/schemas/id_token_test.exs index aa9b806c..e0365bce 100644 --- a/test/boruta/oauth/schemas/id_token_test.exs +++ b/test/boruta/oauth/schemas/id_token_test.exs @@ -19,6 +19,10 @@ defmodule Boruta.Oauth.IdTokenTest do claims end) + stub(Boruta.Support.ResourceOwners, :trust_chain, fn _client -> + {:ok, []} + end) + {:ok, resource_owner: resource_owner, claims: claims} end @@ -40,14 +44,14 @@ defmodule Boruta.Oauth.IdTokenTest do nonce = "nonce" - assert %{ + assert {:ok, %{ sub: "sub", client: ^client, inserted_at: ^inserted_at, scope: "scope", value: value, type: "id_token" - } = IdToken.generate(%{code: code}, nonce) + }} = IdToken.generate(%{code: code}, nonce) signer = Joken.Signer.create("RS512", %{"pem" => client.private_key, "aud" => client.id}) @@ -88,14 +92,14 @@ defmodule Boruta.Oauth.IdTokenTest do nonce = "nonce" - assert %{ + assert {:ok, %{ sub: "sub", client: ^client, inserted_at: ^inserted_at, scope: "scope", value: value, type: "id_token" - } = IdToken.generate(%{token: token}, nonce) + }} = IdToken.generate(%{token: token}, nonce) signer = Joken.Signer.create("RS512", %{"pem" => client.private_key, "aud" => client.id}) @@ -146,14 +150,14 @@ defmodule Boruta.Oauth.IdTokenTest do nonce = "nonce" - assert %{ + assert {:ok, %{ sub: "sub", client: ^client, inserted_at: ^inserted_at, scope: "scope", value: value, type: "id_token" - } = IdToken.generate(%{token: token, code: code}, nonce) + }} = IdToken.generate(%{token: token, code: code}, nonce) signer = Joken.Signer.create("RS512", %{"pem" => client.private_key, "aud" => client.id}) @@ -194,14 +198,14 @@ defmodule Boruta.Oauth.IdTokenTest do nonce = "nonce" - assert %{ + assert {:ok, %{ sub: "sub", client: ^client, inserted_at: ^inserted_at, scope: "scope", value: value, type: "id_token" - } = IdToken.generate(%{base_token: base_token}, nonce) + }} = IdToken.generate(%{base_token: base_token}, nonce) signer = Joken.Signer.create("RS512", %{"pem" => client.private_key, "aud" => client.id}) @@ -240,14 +244,14 @@ defmodule Boruta.Oauth.IdTokenTest do nonce = "nonce" - assert %{ + assert {:ok, %{ sub: "sub", client: ^client, inserted_at: ^inserted_at, scope: "scope", value: value, type: "id_token" - } = IdToken.generate(%{base_token: base_token}, nonce) + }} = IdToken.generate(%{base_token: base_token}, nonce) signer = Joken.Signer.create("RS512", %{"pem" => client.private_key, "aud" => client.id}) @@ -296,14 +300,14 @@ defmodule Boruta.Oauth.IdTokenTest do nonce = "nonce" - assert %{ + assert {:ok, %{ sub: "sub", client: ^client, inserted_at: ^inserted_at, scope: "scope", value: value, type: "id_token" - } = IdToken.generate(%{token: token, code: code}, nonce) + }} = IdToken.generate(%{token: token, code: code}, nonce) signer = Joken.Signer.create("RS256", %{"pem" => client.private_key, "aud" => client.id}) @@ -358,14 +362,14 @@ defmodule Boruta.Oauth.IdTokenTest do nonce = "nonce" - assert %{ + assert {:ok, %{ sub: "sub", client: ^client, inserted_at: ^inserted_at, scope: "scope", value: value, type: "id_token" - } = IdToken.generate(%{token: token, code: code}, nonce) + }} = IdToken.generate(%{token: token, code: code}, nonce) signer = Joken.Signer.create("RS384", %{"pem" => client.private_key, "aud" => client.id}) @@ -420,14 +424,14 @@ defmodule Boruta.Oauth.IdTokenTest do nonce = "nonce" - assert %{ + assert {:ok, %{ sub: "sub", client: ^client, inserted_at: ^inserted_at, scope: "scope", value: value, type: "id_token" - } = IdToken.generate(%{token: token, code: code}, nonce) + }} = IdToken.generate(%{token: token, code: code}, nonce) signer = Joken.Signer.create("HS256", client.secret) @@ -481,14 +485,14 @@ defmodule Boruta.Oauth.IdTokenTest do nonce = "nonce" - assert %{ + assert {:ok, %{ sub: "sub", client: ^client, inserted_at: ^inserted_at, scope: "scope", value: value, type: "id_token" - } = IdToken.generate(%{token: token, code: code}, nonce) + }} = IdToken.generate(%{token: token, code: code}, nonce) signer = Joken.Signer.create("HS384", client.secret) @@ -550,14 +554,14 @@ defmodule Boruta.Oauth.IdTokenTest do nonce = "nonce" - assert %{ + assert {:ok, %{ sub: "sub", client: ^client, inserted_at: ^inserted_at, scope: "scope", value: value, type: "id_token" - } = IdToken.generate(%{token: token, code: code}, nonce) + }} = IdToken.generate(%{token: token, code: code}, nonce) signer = Joken.Signer.create("HS512", client.secret) diff --git a/test/boruta/openid/integration/credential_test.exs b/test/boruta/openid/integration/credential_test.exs index a0cf8db8..1ef464ff 100644 --- a/test/boruta/openid/integration/credential_test.exs +++ b/test/boruta/openid/integration/credential_test.exs @@ -17,6 +17,14 @@ defmodule Boruta.OpenidTest.CredentialTest do setup :verify_on_exit! + setup do + stub(Boruta.Support.ResourceOwners, :trust_chain, fn _client -> + {:ok, []} + end) + + :ok + end + describe "deliver verifiable credentials" do test "returns an error with no access token" do conn = %Plug.Conn{} diff --git a/test/boruta/openid/verifiable_credentials_test.exs b/test/boruta/openid/verifiable_credentials_test.exs index da4e6a9b..2ae85cbf 100644 --- a/test/boruta/openid/verifiable_credentials_test.exs +++ b/test/boruta/openid/verifiable_credentials_test.exs @@ -8,8 +8,13 @@ defmodule Boruta.Openid.VerifiableCredentialsTest do alias Boruta.Oauth.ResourceOwner alias Boruta.Openid.VerifiableCredentials + import Mox + describe "issue_verifiable_credential/4" do setup do + stub(Boruta.Support.ResourceOwners, :trust_chain, fn _client -> + {:ok, []} + end) signer = Joken.Signer.create("RS256", %{"pem" => private_key_fixture()}, %{ "kid" =>