Skip to content

Commit fa260cb

Browse files
committed
[federation] implement resolve endpoint
1 parent ba9f159 commit fa260cb

File tree

8 files changed

+164
-5
lines changed

8 files changed

+164
-5
lines changed

apps/boruta_federation/lib/boruta_federation/federation_entities.ex

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ defmodule BorutaFederation.FederationEntities do
1414

1515
@spec get_entity(id :: Ecto.UUID.t()) :: federation_entity :: FederationEntity.t() | nil
1616
def get_entity(id) do
17-
Repo.get(FederationEntity, id)
17+
case Ecto.UUID.cast(id) do
18+
{:ok, _} ->
19+
Repo.get(FederationEntity, id)
20+
_ ->
21+
nil
22+
end
1823
end
1924

2025
@spec create_entity(
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
defmodule BorutaFederation.OpenidFederationApplication do
2+
@callback resolve_success(context :: any, federation_entity_statement :: String.t()) :: any()
3+
@callback resolve_failure(context :: any, error :: Boruta.Oauth.Error.t()) :: any()
4+
end
5+
6+
defmodule BorutaFederation.OpenidFederation do
7+
alias Boruta.Oauth.Error
8+
alias BorutaFederation.FederationEntities
9+
alias BorutaFederation.TrustChains
10+
11+
@type resolve_params :: %{
12+
sub: String.t(),
13+
anchor: String.t()
14+
}
15+
16+
@spec resolve(context :: any(), resolve_params :: resolve_params(), module :: atom()) :: any()
17+
def resolve(context, resolve_params, module) do
18+
case FederationEntities.get_entity(resolve_params[:sub]) do
19+
nil ->
20+
error = %Error{
21+
status: :bad_request,
22+
error: :invalid_request,
23+
error_description: "Federation entity could not be found."
24+
}
25+
module.resolve_failure(context, error)
26+
27+
entity ->
28+
case TrustChains.generate_statement(entity) do
29+
{:ok, statement} ->
30+
module.resolve_success(context, statement)
31+
32+
{:error, error} ->
33+
error = %Error{
34+
status: :bad_request,
35+
error: :invalid_request,
36+
error_description: "Could not generate federation entity statement #{error}."
37+
}
38+
39+
module.resolve_failure(context, error)
40+
end
41+
end
42+
end
43+
end

apps/boruta_federation/lib/boruta_federation/trust_chains.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ defmodule BorutaFederation.TrustChains do
1616
now = :os.system_time(:second)
1717

1818
payload = %{
19-
"iss" => issuer(),
20-
"sub" => entity.id,
21-
"iat" => now,
2219
"exp" => now + entity.trust_chain_statement_ttl,
20+
"iat" => now,
21+
"iss" => issuer(),
2322
"jwks" => jwks,
2423
"metadata" => metadata,
24+
"sub" => entity.id,
2525
"trust_marks" => trust_marks
2626
}
2727

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
defmodule BorutaFederationWeb.ResolveController do
2+
@behaviour BorutaFederation.OpenidFederationApplication
3+
4+
alias BorutaFederationWeb.ErrorView
5+
use BorutaFederationWeb, :controller
6+
7+
alias BorutaFederation.OpenidFederation
8+
9+
def resolve(conn, params) do
10+
resolve_params = %{
11+
sub: params["sub"],
12+
anchor: params["anchor"]
13+
}
14+
15+
OpenidFederation.resolve(conn, resolve_params, __MODULE__)
16+
end
17+
18+
@impl BorutaFederation.OpenidFederationApplication
19+
def resolve_success(conn, federation_entity_statement) do
20+
conn
21+
|> put_resp_header("content-type", "application/resolve-response+jwt")
22+
|> send_resp(200, federation_entity_statement)
23+
end
24+
25+
@impl BorutaFederation.OpenidFederationApplication
26+
def resolve_failure(conn, error) do
27+
conn
28+
|> put_status(error.status)
29+
|> put_view(ErrorView)
30+
|> render("error.json", error: error)
31+
end
32+
end

apps/boruta_federation/lib/boruta_federation_web/router.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ defmodule BorutaFederationWeb.Router do
1111
end
1212

1313
scope "/", BorutaFederationWeb do
14-
pipe_through :browser
14+
pipe_through :api
1515

1616
get "/", PageController, :index
17+
get "/resolve", ResolveController, :resolve
1718
end
1819
end

apps/boruta_federation/lib/boruta_federation_web/views/error_view.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,11 @@ defmodule BorutaFederationWeb.ErrorView do
1313
def template_not_found(template, _assigns) do
1414
Phoenix.Controller.status_message_from_template(template)
1515
end
16+
17+
def render("error.json", %{error: error}) do
18+
%{
19+
error: error.error,
20+
error_description: error.error_description
21+
}
22+
end
1623
end

apps/boruta_federation/test/boruta_federation/trust_chains_test.exs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule BorutaFederation.TrustChainsTest do
33

44
import BorutaFederation.Factory
55

6+
alias BorutaFederation.FederationEntities.LeafEntity.Token
67
alias BorutaFederation.TrustChains
78

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

1213
assert {:ok, statement} = TrustChains.generate_statement(entity)
1314
assert statement
15+
16+
entity_id = entity.id
17+
18+
assert {:ok,
19+
%{
20+
"exp" => exp,
21+
"iat" => iat,
22+
"iss" => "http://localhost:4000",
23+
"jwks" => [jwk],
24+
"metadata" => %{"openid_provider" => %{"issuer" => "http://localhost:4000"}},
25+
"sub" => ^entity_id,
26+
"trust_marks" => [trust_mark]
27+
}} = Joken.peek_claims(statement)
28+
29+
signer =
30+
Joken.Signer.create(entity.trust_chain_statement_alg, %{
31+
"pem" => JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem() |> elem(1)
32+
})
33+
34+
assert {:ok, _} = Token.verify_and_validate(statement, signer)
35+
assert {:ok, _} = Token.verify_and_validate(trust_mark, signer)
36+
assert iat
37+
assert exp
1438
end
1539
end
1640
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
defmodule BorutaFederationWeb.ResolveControllerTest do
2+
use BorutaFederationWeb.ConnCase
3+
4+
import BorutaFederation.Factory
5+
6+
alias BorutaFederation.FederationEntities.LeafEntity.Token
7+
8+
describe "GET /resolve" do
9+
test "retruns not found", %{conn: conn} do
10+
conn = get(conn, Routes.resolve_path(conn, :resolve, %{sub: "sub", anchor: "anchor"}))
11+
assert json_response(conn, 400) == %{
12+
"error" => "invalid_request",
13+
"error_description" => "Federation entity could not be found."
14+
}
15+
end
16+
17+
test "retruns a statement", %{conn: conn} do
18+
entity = insert(:entity)
19+
20+
conn = get(conn, Routes.resolve_path(conn, :resolve, %{sub: entity.id, anchor: "anchor"}))
21+
assert statement = response(conn, 200)
22+
23+
entity_id = entity.id
24+
25+
assert {:ok,
26+
%{
27+
"exp" => exp,
28+
"iat" => iat,
29+
"iss" => "http://localhost:4000",
30+
"jwks" => [jwk],
31+
"metadata" => %{"openid_provider" => %{"issuer" => "http://localhost:4000"}},
32+
"sub" => ^entity_id,
33+
"trust_marks" => [trust_mark]
34+
}} = Joken.peek_claims(statement)
35+
36+
signer =
37+
Joken.Signer.create(entity.trust_chain_statement_alg, %{
38+
"pem" => JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem() |> elem(1)
39+
})
40+
41+
assert {:ok, _} = Token.verify_and_validate(statement, signer)
42+
assert {:ok, _} = Token.verify_and_validate(trust_mark, signer)
43+
assert iat
44+
assert exp
45+
end
46+
end
47+
end

0 commit comments

Comments
 (0)