Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow federation metadata #49

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ config :samly, Samly.Provider,
| `sp_id` | _(mandatory)_ The service provider definition to be used with this Identity Provider definition |
| `base_url` | _(optional)_ If missing `Samly` will use the current URL to derive this. It is better to define this in production deployment. |
| `metadata_file` | _(mandatory)_ Path to the IdP metadata XML file obtained from the Identity Provider. |
| `entity_id` | _(optional)_ In case metadata file contains federation definition (root element is `EntitiesDescriptor`) this field is necessary. Based on that samly will extract appropriate idp element.
| `pre_session_create_pipeline` | _(optional)_ Check the customization section. |
| `use_redirect_for_req` | _(optional)_ Default is `false`. When this is `false`, `Samly` will POST to the IdP SAML endpoints. |
| `sign_requests`, `sign_metadata` | _(optional)_ Default is `true`. |
Expand Down
94 changes: 64 additions & 30 deletions lib/samly/idp_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,22 @@ defmodule Samly.IdpData do
@post "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"

@entity_id_selector ~x"//#{@entdesc}/@entityID"sl
@nameid_format_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@nameid}/text()"s
@req_signed_selector ~x"//#{@entdesc}/#{@idpdesc}/@#{@signedreq}"s
@sso_redirect_url_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@redirect}']/@Location"s
@sso_post_url_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@post}']/@Location"s
@slo_redirect_url_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@redirect}']/@Location"s
@slo_post_url_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@post}']/@Location"s
@signing_keys_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@keydesc}[@use != 'encryption']"l
@enc_keys_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@keydesc}[@use = 'encryption']"l


# These functions work on EntityDescriptor element
@sso_redirect_url_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@redirect}']/@Location"s
@sso_post_url_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@post}']/@Location"s
@slo_redirect_url_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@redirect}']/@Location"s
@slo_post_url_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@post}']/@Location"s
@nameid_format_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@nameid}/text()[1]"s # TODO How to deal with multiple nameid formats?
@signing_keys_in_idp_selector ~x"./#{@idpdesc}/#{@keydesc}[@use != 'encryption']"l
@cert_selector ~x"./ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()"s

defp entity_by_id_selector(id), do: ~x"/#{@entsdesc}/#{@entdesc}[@entityID = '#{id}'][1]"

@type id :: binary()

@spec load_providers([map], %{required(id()) => %SpData{}}) ::
Expand Down Expand Up @@ -121,9 +127,9 @@ defmodule Samly.IdpData do
end

@spec load_metadata(%IdpData{}, map()) :: %IdpData{}
defp load_metadata(idp_data, _opts_map) do
defp load_metadata(idp_data, opts_map) do
with {:reading, {:ok, raw_xml}} <- {:reading, File.read(idp_data.metadata_file)},
{:parsing, {:ok, idp_data}} <- {:parsing, from_xml(raw_xml, idp_data)} do
{:parsing, {:ok, idp_data}} <- {:parsing, from_xml(raw_xml, idp_data, opts_map)} do
idp_data
else
{:reading, {:error, reason}} ->
Expand Down Expand Up @@ -251,8 +257,8 @@ defmodule Samly.IdpData do
if is_boolean(v), do: Map.put(idp_data, attr_name, v), else: idp_data
end

@spec from_xml(binary, %IdpData{}) :: {:ok, %IdpData{}}
def from_xml(metadata_xml, idp_data) when is_binary(metadata_xml) do
@spec from_xml(binary, %IdpData{}, %{}) :: {:ok, %IdpData{}}
def from_xml(metadata_xml, idp_data, opts) when is_binary(metadata_xml) do
xml_opts = [
space: :normalize,
namespace_conformant: true,
Expand All @@ -261,23 +267,41 @@ defmodule Samly.IdpData do
]

md_xml = SweetXml.parse(metadata_xml, xml_opts)
signing_certs = get_signing_certs(md_xml)

{:ok,
%IdpData{
idp_data
| entity_id: get_entity_id(md_xml),
signed_requests: get_req_signed(md_xml),
certs: signing_certs,
fingerprints: idp_cert_fingerprints(signing_certs),
sso_redirect_url: get_sso_redirect_url(md_xml),
sso_post_url: get_sso_post_url(md_xml),
slo_redirect_url: get_slo_redirect_url(md_xml),
slo_post_url: get_slo_post_url(md_xml),
nameid_format: get_nameid_format(md_xml)
}}

entityID =
case federation_metadata?(opts) do
false -> get_entity_id(md_xml)
true -> opts[:entity_id]
end

entity_md_xml = get_entity_descriptor(md_xml, entityID)



case entity_md_xml do
nil ->
{:error, :entity_not_found}
_ ->
signing_certs = get_signing_certs_in_idp(entity_md_xml)

{:ok,
%IdpData{
idp_data
| entity_id: entityID,
signed_requests: get_req_signed(md_xml),
certs: signing_certs,
fingerprints: idp_cert_fingerprints(signing_certs),
sso_redirect_url: get_sso_redirect_url(entity_md_xml),
sso_post_url: get_sso_post_url(entity_md_xml),
slo_redirect_url: get_slo_redirect_url(entity_md_xml),
slo_post_url: get_slo_post_url(entity_md_xml),
nameid_format: get_nameid_format(entity_md_xml)
}}
end
end

defp federation_metadata?(opts), do: opts[:entity_id] != nil

# @spec to_esaml_idp_metadata(IdpData.t(), map()) :: :esaml_idp_metadata
defp to_esaml_idp_metadata(%IdpData{} = idp_data, %{} = idp_config) do
{sso_url, slo_url} = get_sso_slo_urls(idp_data, idp_config)
Expand Down Expand Up @@ -357,7 +381,7 @@ defmodule Samly.IdpData do
)
end

@spec get_entity_id(:xmlElement) :: binary()
@spec get_entity_id(:xmlElement) :: binary()
def get_entity_id(md_elem) do
md_elem |> xpath(@entity_id_selector |> add_ns()) |> hd() |> String.trim()
end
Expand All @@ -373,11 +397,10 @@ defmodule Samly.IdpData do
@spec get_req_signed(:xmlElement) :: binary()
def get_req_signed(md_elem), do: get_data(md_elem, @req_signed_selector)

@spec get_signing_certs(:xmlElement) :: certs()
def get_signing_certs(md_elem), do: get_certs(md_elem, @signing_keys_selector)
#@spec get_signing_certs(:xmlElement) :: certs()
#def get_signing_certs(md_elem), do: get_certs(md_elem, signing_keys_selector())

@spec get_enc_certs(:xmlElement) :: certs()
def get_enc_certs(md_elem), do: get_certs(md_elem, @enc_keys_selector)
def get_signing_certs_in_idp(md_elem), do: get_certs(md_elem, @signing_keys_in_idp_selector)

@spec get_certs(:xmlElement, %SweetXpath{}) :: certs()
defp get_certs(md_elem, key_selector) do
Expand Down Expand Up @@ -425,4 +448,15 @@ defmodule Samly.IdpData do
|> SweetXml.add_namespace("md", "urn:oasis:names:tc:SAML:2.0:metadata")
|> SweetXml.add_namespace("ds", "http://www.w3.org/2000/09/xmldsig#")
end
end

@spec get_entity_descriptor(:xmlElement, entityID :: binary()) :: :xmlElement | nil
defp get_entity_descriptor(md_xml, entityID) do
selector = entity_by_id_selector(entityID) |> add_ns()
try do
SweetXml.xpath(md_xml, selector)
rescue
_ -> {:error, :entity_not_found}
end
end

end
Loading