Skip to content

Commit

Permalink
[federation] implement resolve endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
patatoid committed Nov 5, 2024
1 parent ba9f159 commit fa260cb
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ defmodule BorutaFederation.FederationEntities do

@spec get_entity(id :: Ecto.UUID.t()) :: federation_entity :: FederationEntity.t() | nil
def get_entity(id) do
Repo.get(FederationEntity, id)
case Ecto.UUID.cast(id) do
{:ok, _} ->
Repo.get(FederationEntity, id)
_ ->
nil
end
end

@spec create_entity(
Expand Down
43 changes: 43 additions & 0 deletions apps/boruta_federation/lib/boruta_federation/openid_federation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
defmodule BorutaFederation.OpenidFederationApplication do
@callback resolve_success(context :: any, federation_entity_statement :: String.t()) :: any()
@callback resolve_failure(context :: any, error :: Boruta.Oauth.Error.t()) :: any()
end

defmodule BorutaFederation.OpenidFederation do
alias Boruta.Oauth.Error
alias BorutaFederation.FederationEntities
alias BorutaFederation.TrustChains

@type resolve_params :: %{
sub: String.t(),
anchor: String.t()
}

@spec resolve(context :: any(), resolve_params :: resolve_params(), module :: atom()) :: any()
def resolve(context, resolve_params, module) do
case FederationEntities.get_entity(resolve_params[:sub]) do
nil ->
error = %Error{
status: :bad_request,
error: :invalid_request,
error_description: "Federation entity could not be found."
}
module.resolve_failure(context, error)

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

{:error, error} ->
error = %Error{
status: :bad_request,
error: :invalid_request,
error_description: "Could not generate federation entity statement #{error}."
}

module.resolve_failure(context, error)
end
end
end
end
6 changes: 3 additions & 3 deletions apps/boruta_federation/lib/boruta_federation/trust_chains.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ defmodule BorutaFederation.TrustChains do
now = :os.system_time(:second)

payload = %{
"iss" => issuer(),
"sub" => entity.id,
"iat" => now,
"exp" => now + entity.trust_chain_statement_ttl,
"iat" => now,
"iss" => issuer(),
"jwks" => jwks,
"metadata" => metadata,
"sub" => entity.id,
"trust_marks" => trust_marks
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule BorutaFederationWeb.ResolveController do
@behaviour BorutaFederation.OpenidFederationApplication

alias BorutaFederationWeb.ErrorView
use BorutaFederationWeb, :controller

alias BorutaFederation.OpenidFederation

def resolve(conn, params) do
resolve_params = %{
sub: params["sub"],
anchor: params["anchor"]
}

OpenidFederation.resolve(conn, resolve_params, __MODULE__)
end

@impl BorutaFederation.OpenidFederationApplication
def resolve_success(conn, federation_entity_statement) do
conn
|> put_resp_header("content-type", "application/resolve-response+jwt")
|> send_resp(200, federation_entity_statement)
end

@impl BorutaFederation.OpenidFederationApplication
def resolve_failure(conn, error) do
conn
|> put_status(error.status)
|> put_view(ErrorView)
|> render("error.json", error: error)
end
end
3 changes: 2 additions & 1 deletion apps/boruta_federation/lib/boruta_federation_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ defmodule BorutaFederationWeb.Router do
end

scope "/", BorutaFederationWeb do
pipe_through :browser
pipe_through :api

get "/", PageController, :index
get "/resolve", ResolveController, :resolve
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@ defmodule BorutaFederationWeb.ErrorView do
def template_not_found(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end

def render("error.json", %{error: error}) do
%{
error: error.error,
error_description: error.error_description
}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule BorutaFederation.TrustChainsTest do

import BorutaFederation.Factory

alias BorutaFederation.FederationEntities.LeafEntity.Token
alias BorutaFederation.TrustChains

describe "generate_statement/1" do
Expand All @@ -11,6 +12,29 @@ defmodule BorutaFederation.TrustChainsTest do

assert {:ok, statement} = TrustChains.generate_statement(entity)
assert statement

entity_id = entity.id

assert {:ok,
%{
"exp" => exp,
"iat" => iat,
"iss" => "http://localhost:4000",
"jwks" => [jwk],
"metadata" => %{"openid_provider" => %{"issuer" => "http://localhost:4000"}},
"sub" => ^entity_id,
"trust_marks" => [trust_mark]
}} = Joken.peek_claims(statement)

signer =
Joken.Signer.create(entity.trust_chain_statement_alg, %{
"pem" => JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem() |> elem(1)
})

assert {:ok, _} = Token.verify_and_validate(statement, signer)
assert {:ok, _} = Token.verify_and_validate(trust_mark, signer)
assert iat
assert exp
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
defmodule BorutaFederationWeb.ResolveControllerTest do
use BorutaFederationWeb.ConnCase

import BorutaFederation.Factory

alias BorutaFederation.FederationEntities.LeafEntity.Token

describe "GET /resolve" do
test "retruns not found", %{conn: conn} do
conn = get(conn, Routes.resolve_path(conn, :resolve, %{sub: "sub", anchor: "anchor"}))
assert json_response(conn, 400) == %{
"error" => "invalid_request",
"error_description" => "Federation entity could not be found."
}
end

test "retruns a statement", %{conn: conn} do
entity = insert(:entity)

conn = get(conn, Routes.resolve_path(conn, :resolve, %{sub: entity.id, anchor: "anchor"}))
assert statement = response(conn, 200)

entity_id = entity.id

assert {:ok,
%{
"exp" => exp,
"iat" => iat,
"iss" => "http://localhost:4000",
"jwks" => [jwk],
"metadata" => %{"openid_provider" => %{"issuer" => "http://localhost:4000"}},
"sub" => ^entity_id,
"trust_marks" => [trust_mark]
}} = Joken.peek_claims(statement)

signer =
Joken.Signer.create(entity.trust_chain_statement_alg, %{
"pem" => JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem() |> elem(1)
})

assert {:ok, _} = Token.verify_and_validate(statement, signer)
assert {:ok, _} = Token.verify_and_validate(trust_mark, signer)
assert iat
assert exp
end
end
end

0 comments on commit fa260cb

Please sign in to comment.