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 17, 2024
1 parent e2df2fc commit 90f4e5d
Show file tree
Hide file tree
Showing 18 changed files with 225 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<div class="field">
<label>federation entity</label>
<select v-model="currentFederationEntityId">
<option></option>
<option v-for="federationEntityOption in federationEntities" :value="federationEntityOption.id" :key="federationEntityOption.id" :selected="federationEntityOption.id == currentFederationEntityId">
{{ federationEntityOption.organization_name }}
</option>
Expand Down Expand Up @@ -31,7 +32,7 @@ export default {
return this.federationEntity.id
},
set (federationEntityId) {
return this.$emit('federationEntityChange', this.federationEntities.find(({ id }) => id === federationEntityId))
return this.$emit('federationEntityChange', this.federationEntities.find(({ id }) => id === federationEntityId) || new FederationEntity())
}
}
}
Expand Down
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
14 changes: 4 additions & 10 deletions apps/boruta_admin/lib/boruta_admin/clients.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,10 @@ defmodule BorutaAdmin.Clients do
)
end),
{:ok, _client_federation_entity} <-
(case federation_entity_id do
nil ->
{:ok, nil}

federation_entity_id ->
FederationEntities.upsert_client_federation_entity(
client.id,
federation_entity_id
)
end) do
FederationEntities.upsert_client_federation_entity(
client.id,
federation_entity_id
) do
client
else
{:error, error} ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule BorutaFederation.Application do
@impl true
def start(_type, _args) do
children = [
BorutaFederation.Cache,
BorutaFederation.Repo,
BorutaFederationWeb.Endpoint
]
Expand Down
5 changes: 5 additions & 0 deletions apps/boruta_federation/lib/boruta_federation/cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule BorutaFederation.Cache do
use Nebulex.Cache,
otp_app: :boruta_federation,
adapter: Nebulex.Adapters.Replicated
end
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule BorutaFederation.FederationEntities do
case Ecto.UUID.cast(id) do
{:ok, _} ->
Repo.get(FederationEntity, id)

_ ->
nil
end
Expand Down Expand Up @@ -55,7 +56,12 @@ defmodule BorutaFederation.FederationEntities do
) ::
{:ok, client_federation_entity :: ClientFederationEntity.t() | nil}
| {:error, changeset :: Ecto.Changeset.t()}
def upsert_client_federation_entity(_client_id, nil), do: {:ok, nil}
def upsert_client_federation_entity(client_id, nil) do
with {1, _} <-
Repo.delete_all(from cfe in ClientFederationEntity, where: cfe.client_id == ^client_id) do
{:ok, nil}
end
end

def upsert_client_federation_entity(client_id, federation_entity_id) do
%ClientFederationEntity{}
Expand All @@ -69,7 +75,8 @@ defmodule BorutaFederation.FederationEntities do
)
end

@spec get_federation_entity_by_client_id(client_id :: String.t()) :: federation_entity :: FederationEntity.t() | nil
@spec get_federation_entity_by_client_id(client_id :: String.t()) ::
federation_entity :: FederationEntity.t() | nil
def get_federation_entity_by_client_id(client_id) do
case Ecto.UUID.cast(client_id) do
{:ok, client_id} ->
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) 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
Loading

0 comments on commit 90f4e5d

Please sign in to comment.