Skip to content

Commit

Permalink
[federation] resolve trust chains
Browse files Browse the repository at this point in the history
  • Loading branch information
patatoid committed Nov 11, 2024
1 parent a26721e commit 8bd9177
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@
<option v-for="type in entityTypes" :value="type">{{ type }}</option>
</select>
</div>
<h3>Authorities</h3>
<div class="field" :class="{ 'error': federationEntity.errors?.authorities }">
<label>Authorities</label>
<div v-for="(authority, index) in federationEntity.authorities" class="field" :key="index">
<div class="ui right icon input">
<input type="text" v-model="authority.url" placeholder="http://authority.uri" />
<i v-on:click="deleteAuthority(authority)" class="close icon"></i>
<div v-for="(authority, index) in federationEntity.authorities" class="ui segment field" :key="index">
<i v-on:click="deleteAuthority(authority)" class="close icon"></i>
<div class="field">
<label>Issuer</label>
<input type="text" v-model="authority.issuer" placeholder="http://authority.uri" />
</div>
<div class="field">
<label>Subject</label>
<input type="text" v-model="authority.sub" placeholder="sub" />
</div>
</div>
<a v-on:click.prevent="addAuthority()" class="ui blue fluid button">Add an authority</a>
Expand Down Expand Up @@ -97,17 +102,19 @@ export default {
<style scoped lang="scss">
.federation-entity-form {
.segment .close {
z-index: 10;
cursor: pointer;
position: absolute;
top: 1em;
right: 1em;
}
.field {
position: relative;
&.federation-entitys input {
margin-right: 3em;
}
}
.ui.icon.input>i.icon.close {
cursor: pointer;
pointer-events: all;
position: absolute;
}
.authorized-scopes-select {
margin-right: 3em;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const assign = {
id: function ({ id }) { this.id = id },
organization_name: function ({ organization_name }) { this.organization_name = organization_name },
type: function ({ type }) { this.type = type },
authorities: function ({ authorities }) { this.authorities = authorities.map((url) => ({ url })) },
authorities: function ({ authorities }) { this.authorities = authorities },
is_default: function ({ is_default }) { this.is_default = is_default },
trust_chain_statement_alg: function ({ trust_chain_statement_alg }) { this.trust_chain_statement_alg = trust_chain_statement_alg },
trust_chain_statement_ttl: function ({ trust_chain_statement_ttl }) { this.trust_chain_statement_ttl = trust_chain_statement_ttl },
Expand Down Expand Up @@ -98,7 +98,7 @@ class FederationEntity {
return {
id,
organization_name,
authorities: authorities.map(({ url }) => url),
authorities: authorities,
type,
trust_chain_statement_alg,
trust_chain_statement_ttl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</div>
<div class="item">
<span class="header">Authorities</span>
<span class="description" v-for="authority in federationEntity.authorities">{{ authority.url }}</span>
<span class="description" v-for="authority in federationEntity.authorities">{{ authority.issuer }} - {{ authority.sub }}</span>
</div>
<div class="item">
<span class="header">Trust mark logo uri</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ defmodule BorutaFederation.FederationEntities.FederationEntity do
]
}

@authority_schema %{
"type" => "object",
"properties" => %{
"issuer" => %{"type" => "string"},
"sub" => %{"type" => "string"}
},
"required" => ["issuer", "sub"]
}

@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "federation_entities" do
Expand All @@ -69,7 +78,7 @@ defmodule BorutaFederation.FederationEntities.FederationEntity do
field(:private_key, :string)
field(:trust_chain_statement_alg, :string)
field(:trust_chain_statement_ttl, :integer, default: 3600 * 24)
field(:authorities, {:array, :string}, default: [])
field(:authorities, {:array, :map}, default: [])
field(:default, :boolean, default: false)
field(:trust_mark_logo_uri, :string)
field(:key_pair_type, :map,
Expand Down Expand Up @@ -157,10 +166,23 @@ defmodule BorutaFederation.FederationEntities.FederationEntity do
end

defp validate_authorities(changeset) do
validate_change(changeset, :authorities, fn field, values ->
Enum.map(values, &validate_url/1)
|> Enum.reject(&is_nil/1)
|> Enum.map(fn error -> {field, error} end)
Enum.reduce(get_field(changeset, :authorities), changeset, fn authority, changeset ->
case ExJsonSchema.Validator.validate(
@authority_schema,
authority,
error_formatter: BorutaFormatter
) do
:ok ->
case validate_url(authority["issuer"]) do
nil ->
changeset
error ->
add_error(changeset, :authorities, error)
end

{:error, errors} ->
add_error(changeset, :authorities, "validation failed: #{Enum.join(errors, " ")}")
end
end)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ defmodule BorutaFederation.FederationEntities.LeafEntity do

import Boruta.Config, only: [issuer: 0]

@federation_configuration_path "/.well-known/openid-federation"

defmodule Token do
@moduledoc false

Expand All @@ -15,12 +17,13 @@ defmodule BorutaFederation.FederationEntities.LeafEntity do

@spec metadata(entity :: FederationEntity.t()) :: {:ok, metadata :: map()}
def metadata(entity) do
{:ok, %{
"openid_provider" => %{
"issuer" => issuer(),
"organization_name" => entity.organization_name
}
}}
{:ok,
%{
"openid_provider" => %{
"issuer" => issuer(),
"organization_name" => entity.organization_name
}
}}
end

@spec jwks(entity :: FederationEntity.t()) :: {:ok, jwks :: map()}
Expand All @@ -43,11 +46,92 @@ defmodule BorutaFederation.FederationEntities.LeafEntity do
case Joken.encode_and_sign(payload, signer) do
{:ok, trust_mark, _payload} ->
{:ok, [trust_mark]}

{:error, error} ->
{:error, to_string(error)}
end
end

@spec resolve_parents_chain(entity :: FederationEntity.t()) :: {:ok, chain :: list(String.t())}
def resolve_parents_chain(_entity), do: {:ok, []}
def resolve_parents_chain(entity) do
Enum.reduce_while(entity.authorities, {:ok, []}, fn authority, {:ok, acc} ->
case resolve_chain(authority) do
{:ok, statement, trust_chain} ->
case Enum.reduce_while(
trust_chain ++ [statement],
{:ok, acc},
fn statement, {:ok, acc} ->
# TODO
{:ok, %{"sub" => sub}} = Joken.peek_claims(statement)

case fetch_statement(authority, sub) do
{:ok, statement} ->
{:cont, {:ok, acc ++ [statement]}}

{:error, error} ->
{:halt, {:error, error}}
end
end
) do
{:ok, chain} ->
{:cont, {:ok, acc ++ chain}}
{:error, error} ->
{:halt, {:error, error}}
end

_ ->
{:halt, {:error, "Could not fetch parent chain."}}
end
end)
end

defp resolve_chain(authority) do
with {:ok, %Finch.Response{status: 200, body: configuration}} <- Finch.build(:get, authority["issuer"] <> @federation_configuration_path) |> Finch.request(OpenIDHttpClient),
{:ok, %{"federation_resolve_endpoint" => resolve_url}} <- Jason.decode(configuration) do
case Finch.build(:get, resolve_url <> "?sub=#{authority["sub"]}") |> Finch.request(OpenIDHttpClient) |> dbg do
{:ok, %Finch.Response{status: 200, body: statement}} ->
with {:ok, %{"jwks" => [jwk], "trust_chain" => trust_chain}} <-
Joken.peek_claims(statement),
{:ok, %{"alg" => alg}} <- Joken.peek_header(statement),
{_, pem} <- JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem(),
signer <- Joken.Signer.create(alg, %{"pem" => pem}),
{:ok, _claims} <- Token.verify_and_validate(statement, signer) do
{:ok, statement, trust_chain}
else
{:ok, []} ->
{:ok, statement, []}

_ ->
{:error, "Could not resolve parent trust chain."}
end

_ ->
{:error, "Could not resolve #{authority["issuer"]} parent trust chain."}
end
else
_ ->
{:error, "Could not resolve #{authority["issuer"]} configuration."}
end
end

defp fetch_statement(authority, sub) do
with {:ok, %Finch.Response{status: 200, body: configuration}} <- Finch.build(:get, authority["issuer"] <> @federation_configuration_path) |> Finch.request(OpenIDHttpClient),
{:ok, %{"federation_fetch_endpoint" => fetch_url}} <- Jason.decode(configuration) do
with {:ok, %Finch.Response{status: 200, body: statement}} <-
Finch.build(:get, fetch_url <> "?sub=#{sub}") |> Finch.request(OpenIDHttpClient),
{:ok, %{"jwks" => [jwk]}} <- Joken.peek_claims(statement),
{:ok, %{"alg" => alg}} <- Joken.peek_header(statement),
{_, pem} <- JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem(),
signer <- Joken.Signer.create(alg, %{"pem" => pem}),
{:ok, _claims} <- Token.verify_and_validate(statement, signer) do
{:ok, statement}
else
_ ->
{:error, "Could not fetch #{authority["issuer"]} statement"}
end
else
_ ->
{:error, "Could not resolve #{authority["issuer"]} configuration."}
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ defmodule BorutaFederation.OpenidFederation do
module.resolve_failure(context, error)

entity ->
case TrustChains.generate_statement(entity) do
case TrustChains.generate_statement(entity, include_trust_chain: true) do
{:ok, statement} ->
module.resolve_success(context, statement)

Expand Down
25 changes: 20 additions & 5 deletions apps/boruta_federation/lib/boruta_federation/trust_chains.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule BorutaFederation.TrustChains do

@spec generate_statement(entity :: FederationEntity.t()) ::
{:ok, trust_chain :: list(String.t())} | {:error, reason :: String.t()}
def generate_statement(entity) do
def generate_statement(entity, opts \\ []) do
signer = Joken.Signer.create(entity.trust_chain_statement_alg, %{"pem" => entity.private_key})

with {:ok, metadata} <- apply(String.to_atom(entity.type), :metadata, [entity]),
Expand All @@ -25,11 +25,26 @@ defmodule BorutaFederation.TrustChains do
"trust_marks" => trust_marks
}

case Joken.encode_and_sign(payload, signer) do
{:ok, statement, _payload} ->
{:ok, statement}
with true <- opts[:include_trust_chain] |> dbg,
{:ok, chain_statements} <- apply(String.to_atom(entity.type), :resolve_parents_chain, [entity]) do
payload = Map.put(payload, "trust_chain", chain_statements)

case Joken.encode_and_sign(payload, signer) do
{:ok, statement, _payload} ->
{:ok, statement}
{:error, error} ->
{:error, to_string(error)}
end
else
{:error, error} ->
{:error, to_string(error)}
{:error, error}
_ ->
case Joken.encode_and_sign(payload, signer) do
{:ok, statement, _payload} ->
{:ok, statement}
{:error, error} ->
{:error, to_string(error)}
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule BorutaFederationWeb.OpenidController do
use BorutaFederationWeb, :controller

def well_known(conn, _params) do
render(conn, "well_known.json")
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule BorutaFederationWeb.OpenidView do
use BorutaFederationWeb, :view

def render("well_known.json", _params) do
# TODO change to the default entity statement
%{
federation_fetch_endpoint: Routes.fetch_url(BorutaFederationWeb.Endpoint, :fetch),
federation_resolve_endpoint: Routes.resolve_url(BorutaFederationWeb.Endpoint, :resolve)
}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule BorutaFederation.Repo.Migrations.AddAuthoritiesToFederationEntities do

def change do
alter table(:federation_entities) do
add :authorities, {:array, :string}, null: false, default: []
add :authorities, {:array, :jsonb}, null: false, default: []
add :default, :boolean, null: false, default: false
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ defmodule BorutaIdentity.IdentityProviders.Backend do
alias BorutaIdentity.Accounts.User
alias BorutaIdentity.Repo
alias BorutaIdentityWeb.Router.Helpers, as: Routes
alias ExJsonSchema.Validator.Error.BorutaFormatter

@type t :: %__MODULE__{
type: String.t(),
Expand Down Expand Up @@ -570,7 +571,7 @@ defmodule BorutaIdentity.IdentityProviders.Backend do
defp validate_metadata_fields(
%Ecto.Changeset{changes: %{metadata_fields: metadata_fields}} = changeset
) do
case ExJsonSchema.Validator.validate(@metadata_fields_schema, metadata_fields) do
case ExJsonSchema.Validator.validate(@metadata_fields_schema, metadata_fields, error_formatter: BorutaFormatter) do
:ok ->
changeset

Expand All @@ -587,7 +588,7 @@ defmodule BorutaIdentity.IdentityProviders.Backend do
%Ecto.Changeset{changes: %{federated_servers: federated_servers}} = changeset
) do
Enum.reduce(federated_servers, changeset, fn federated_server, changeset ->
case ExJsonSchema.Validator.validate(@federated_server_schema, federated_server) do
case ExJsonSchema.Validator.validate(@federated_server_schema, federated_server, error_formatter: BorutaFormatter) do
:ok ->
changeset

Expand All @@ -605,7 +606,7 @@ defmodule BorutaIdentity.IdentityProviders.Backend do
%Ecto.Changeset{changes: %{verifiable_credentials: verifiable_credentials}} = changeset
) do
Enum.reduce(verifiable_credentials, changeset, fn verifiable_credential, changeset ->
case ExJsonSchema.Validator.validate(@verifiable_credential_schema, verifiable_credential) do
case ExJsonSchema.Validator.validate(@verifiable_credential_schema, verifiable_credential, error_formatter: BorutaFormatter) do
:ok ->
changeset

Expand All @@ -623,7 +624,7 @@ defmodule BorutaIdentity.IdentityProviders.Backend do
%Ecto.Changeset{changes: %{verifiable_presentations: verifiable_presentations}} = changeset
) do
Enum.reduce(verifiable_presentations, changeset, fn verifiable_presentation, changeset ->
case ExJsonSchema.Validator.validate(@verifiable_presentation_schema, verifiable_presentation) do
case ExJsonSchema.Validator.validate(@verifiable_presentation_schema, verifiable_presentation, error_formatter: BorutaFormatter) do
:ok ->
case Jason.decode(verifiable_presentation["presentation_definition"]) do
{:ok, _map} ->
Expand Down
6 changes: 6 additions & 0 deletions apps/boruta_web/lib/boruta_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ defmodule BorutaWeb.Router do
get("/.well-known/openid-credential-issuer", OpenidController, :openid_credential_issuer)
end

scope "/", BorutaFederationWeb do
pipe_through(:api)

get("/.well-known/openid-federation", OpenidController, :well_known)
end

get("/healthcheck", BorutaWeb.MonitoringController, :healthcheck, log: false)

forward("/accounts", BorutaIdentityWeb.Endpoint)
Expand Down

0 comments on commit 8bd9177

Please sign in to comment.