From 7a310233fe98225ee6d6e3438668af6437408bbe Mon Sep 17 00:00:00 2001 From: Dominik Stanaszek Date: Fri, 14 Jun 2019 15:05:42 +0200 Subject: [PATCH 1/5] Get some of the data for particulat ED --- lib/samly/idp_data.ex | 89 +++++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/lib/samly/idp_data.ex b/lib/samly/idp_data.ex index 0aa662c..bb3a161 100644 --- a/lib/samly/idp_data.ex +++ b/lib/samly/idp_data.ex @@ -62,13 +62,13 @@ defmodule Samly.IdpData do valid?: boolean() } - @entdesc "md:EntityDescriptor" - @idpdesc "md:IDPSSODescriptor" + @entdesc "EntityDescriptor" + @idpdesc "IDPSSODescriptor" @signedreq "WantAuthnRequestsSigned" @nameid "md:NameIDFormat" @keydesc "md:KeyDescriptor" - @ssos "md:SingleSignOnService" - @slos "md:SingleLogoutService" + @ssos "SingleSignOnService" + @slos "SingleLogoutService" @redirect "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" @post "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" @@ -83,6 +83,13 @@ defmodule Samly.IdpData do @enc_keys_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@keydesc}[@use = 'encryption']"l @cert_selector ~x"./ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()"s + + defp entity_by_id_selector(id), do: ~x"/EntitiesDescriptor/EntityDescriptor[@entityID = '#{id}'][1]" # TODO some EntityDescriptor are prefixed with "md:" + defp sso_redirect_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@redirect}']/@Location"s + defp sso_post_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@post}']/@Location"s + defp slo_redirect_url_selector, do: ~x"/#{@entdesc}/#{@slos}[@Binding = '#{@redirect}']/@Location"s + defp slo_post_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@post}']/@Location"s + @type id :: binary() @spec load_providers([map], %{required(id()) => %SpData{}}) :: @@ -121,9 +128,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}} -> @@ -251,8 +258,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, @@ -261,21 +268,45 @@ defmodule Samly.IdpData do ] md_xml = SweetXml.parse(metadata_xml, xml_opts) + + entityID = case opts[:entity_id] do + nil -> # not provided, we assume file has only one EntityDescriptor + get_entity_id(md_xml) + e -> + e + end + + IO.puts "entity_md_xml:" + entity_md_xml = get_entity_descriptor(md_xml, entityID) |> IO.inspect 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) - }} + IO.puts "slo_redirect_url:" + slo_redirect_url = get_slo_redirect_url(entity_md_xml) |> IO.inspect + IO.puts "slo_post_url:" + slo_post_url = get_slo_post_url(entity_md_xml) |> IO.inspect + IO.puts "sso_redirect_url:" + sso_redirect_url = get_sso_redirect_url(entity_md_xml) |> IO.inspect + IO.puts "sso_post_url:" + sso_post_url = get_sso_post_url(entity_md_xml) |> IO.inspect + + case entity_md_xml do + nil -> + {:error, :entity_not_found} + _ -> + {: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: slo_redirect_url, + slo_post_url: slo_post_url, + nameid_format: get_nameid_format(entity_md_xml) + }} + end end # @spec to_esaml_idp_metadata(IdpData.t(), map()) :: :esaml_idp_metadata @@ -395,16 +426,16 @@ defmodule Samly.IdpData do end @spec get_sso_redirect_url(:xmlElement) :: url() - def get_sso_redirect_url(md_elem), do: get_url(md_elem, @sso_redirect_url_selector) + def get_sso_redirect_url(md_elem), do: get_url(md_elem, sso_redirect_url_selector()) @spec get_sso_post_url(:xmlElement) :: url() - def get_sso_post_url(md_elem), do: get_url(md_elem, @sso_post_url_selector) + def get_sso_post_url(md_elem), do: get_url(md_elem, sso_post_url_selector()) @spec get_slo_redirect_url(:xmlElement) :: url() - def get_slo_redirect_url(md_elem), do: get_url(md_elem, @slo_redirect_url_selector) + def get_slo_redirect_url(md_elem), do: get_url(md_elem, slo_redirect_url_selector()) @spec get_slo_post_url(:xmlElement) :: url() - def get_slo_post_url(md_elem), do: get_url(md_elem, @slo_post_url_selector) + def get_slo_post_url(md_elem), do: get_url(md_elem, slo_post_url_selector()) @spec get_url(:xmlElement, %SweetXpath{}) :: url() defp get_url(md_elem, selector) do @@ -425,4 +456,12 @@ 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 + + @spec get_entity_descriptor(:xmlElement, entityID :: binary()) :: :xmlElement | nil + defp get_entity_descriptor(md_xml, entityID) do + IO.puts "Using selector:" + selector = entity_by_id_selector(entityID) |> IO.inspect + SweetXml.xpath(md_xml, selector) + end + end From 47d9a4b881f292b45d1d612b9ca9c1981afda82e Mon Sep 17 00:00:00 2001 From: Dominik Stanaszek Date: Sat, 22 Jun 2019 11:20:05 +0200 Subject: [PATCH 2/5] Allow federation metadata --- lib/samly/idp_data.ex | 70 ++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/lib/samly/idp_data.ex b/lib/samly/idp_data.ex index bb3a161..18ac1cd 100644 --- a/lib/samly/idp_data.ex +++ b/lib/samly/idp_data.ex @@ -62,11 +62,12 @@ defmodule Samly.IdpData do valid?: boolean() } + @entsdesc "EntitiesDescriptor" @entdesc "EntityDescriptor" @idpdesc "IDPSSODescriptor" @signedreq "WantAuthnRequestsSigned" - @nameid "md:NameIDFormat" - @keydesc "md:KeyDescriptor" + @nameid "NameIDFormat" + @keydesc "KeyDescriptor" @ssos "SingleSignOnService" @slos "SingleLogoutService" @redirect "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" @@ -79,17 +80,24 @@ defmodule Samly.IdpData do @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 - @cert_selector ~x"./ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()"s + @signing_keys_selector ~x"/#{@entsdesc}/#{@idpdesc}/#{@keydesc}[@use != 'encryption']"l + @cert_selector ~x"/#{@entsdesc}/ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()"s - defp entity_by_id_selector(id), do: ~x"/EntitiesDescriptor/EntityDescriptor[@entityID = '#{id}'][1]" # TODO some EntityDescriptor are prefixed with "md:" + + defp entity_by_id_selector(id), do: ~x"//#{@entdesc}[@entityID = '#{id}'][1]" # TODO some EntityDescriptor are prefixed with "md:" defp sso_redirect_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@redirect}']/@Location"s defp sso_post_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@post}']/@Location"s defp slo_redirect_url_selector, do: ~x"/#{@entdesc}/#{@slos}[@Binding = '#{@redirect}']/@Location"s defp slo_post_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@post}']/@Location"s + + defp nameid_format_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@nameid}[1]/text()"s # TODO for now [1]. + + #defp signing_keys_selector, do: ~x"./ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()[@use != 'encryption']"l + defp signing_keys_in_idp_selector, do: ~x"./#{@idpdesc}/#{@keydesc}[@use != 'encryption']"l + defp cert_selector, do: ~x"./ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()"s + @type id :: binary() @spec load_providers([map], %{required(id()) => %SpData{}}) :: @@ -276,23 +284,29 @@ defmodule Samly.IdpData do e end - IO.puts "entity_md_xml:" - entity_md_xml = get_entity_descriptor(md_xml, entityID) |> IO.inspect - signing_certs = get_signing_certs(md_xml) + entity_md_xml = get_entity_descriptor(md_xml, entityID) - IO.puts "slo_redirect_url:" - slo_redirect_url = get_slo_redirect_url(entity_md_xml) |> IO.inspect - IO.puts "slo_post_url:" - slo_post_url = get_slo_post_url(entity_md_xml) |> IO.inspect - IO.puts "sso_redirect_url:" - sso_redirect_url = get_sso_redirect_url(entity_md_xml) |> IO.inspect - IO.puts "sso_post_url:" - sso_post_url = get_sso_post_url(entity_md_xml) |> IO.inspect + case entity_md_xml do nil -> {:error, :entity_not_found} _ -> + signing_certs = get_signing_certs_in_idp(entity_md_xml) + IO.puts "Got #{signing_certs |> length} signing_certs from #{entityID}" + + IO.puts "slo_redirect_url:" + slo_redirect_url = get_slo_redirect_url(entity_md_xml) |> IO.inspect + IO.puts "slo_post_url:" + slo_post_url = get_slo_post_url(entity_md_xml) |> IO.inspect + IO.puts "sso_redirect_url:" + sso_redirect_url = get_sso_redirect_url(entity_md_xml) |> IO.inspect + IO.puts "sso_post_url:" + sso_post_url = get_sso_post_url(entity_md_xml) |> IO.inspect + + IO.puts "nameid_format:" + nameid_format = get_nameid_format(entity_md_xml) |> IO.inspect + {:ok, %IdpData{ idp_data @@ -304,7 +318,7 @@ defmodule Samly.IdpData do sso_post_url: get_sso_post_url(entity_md_xml), slo_redirect_url: slo_redirect_url, slo_post_url: slo_post_url, - nameid_format: get_nameid_format(entity_md_xml) + nameid_format: nameid_format }} end end @@ -395,7 +409,7 @@ defmodule Samly.IdpData do @spec get_nameid_format(:xmlElement) :: nameid_format() def get_nameid_format(md_elem) do - case get_data(md_elem, @nameid_format_selector) do + case get_data(md_elem, nameid_format_selector()) do "" -> :unknown nameid_format -> to_charlist(nameid_format) end @@ -404,11 +418,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 @@ -416,7 +429,7 @@ defmodule Samly.IdpData do |> xpath(key_selector |> add_ns()) |> Enum.map(fn e -> # Extract base64 encoded cert from XML (strip away any whitespace) - cert = xpath(e, @cert_selector |> add_ns()) + cert = xpath(e, cert_selector() |> add_ns()) cert |> String.split() @@ -459,9 +472,12 @@ defmodule Samly.IdpData do @spec get_entity_descriptor(:xmlElement, entityID :: binary()) :: :xmlElement | nil defp get_entity_descriptor(md_xml, entityID) do - IO.puts "Using selector:" - selector = entity_by_id_selector(entityID) |> IO.inspect - SweetXml.xpath(md_xml, selector) + selector = entity_by_id_selector(entityID) + try do + SweetXml.xpath(md_xml, selector) + rescue + e -> {:error, :entity_not_found} + end end end From bfb54df93b315a544f858866e6df27a5b500b0aa Mon Sep 17 00:00:00 2001 From: Dominik Stanaszek Date: Mon, 24 Jun 2019 16:04:19 +0200 Subject: [PATCH 3/5] Fix incorrect slo_redirect xpath --- lib/samly/idp_data.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/samly/idp_data.ex b/lib/samly/idp_data.ex index 18ac1cd..e0e74f0 100644 --- a/lib/samly/idp_data.ex +++ b/lib/samly/idp_data.ex @@ -88,7 +88,7 @@ defmodule Samly.IdpData do defp entity_by_id_selector(id), do: ~x"//#{@entdesc}[@entityID = '#{id}'][1]" # TODO some EntityDescriptor are prefixed with "md:" defp sso_redirect_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@redirect}']/@Location"s defp sso_post_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@post}']/@Location"s - defp slo_redirect_url_selector, do: ~x"/#{@entdesc}/#{@slos}[@Binding = '#{@redirect}']/@Location"s + defp slo_redirect_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@redirect}']/@Location"s defp slo_post_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@post}']/@Location"s From 6fbf06d85ee3e713c9bf177ae09e3f722ea858cf Mon Sep 17 00:00:00 2001 From: Dominik Stanaszek Date: Mon, 24 Jun 2019 16:04:42 +0200 Subject: [PATCH 4/5] Add possibility of simple logout For now we only logout the user from our service. We don't use so called SLO of shibboleth (that would allow us to log user out of idp and all SPs they are logged into). --- lib/samly.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/samly.ex b/lib/samly.ex index e390abf..601e6e8 100644 --- a/lib/samly.ex +++ b/lib/samly.ex @@ -59,4 +59,9 @@ defmodule Samly do def get_attribute(%Assertion{} = assertion, name) do Map.get(assertion.computed, name) || Map.get(assertion.attributes, name) end + + def logout(conn) do + conn + |> Conn.delete_session("samly_assertion_key") + end end From 29a9e450fb1b8c11ea65942e419b63f9083e6101 Mon Sep 17 00:00:00 2001 From: Dominik Stanaszek Date: Tue, 25 Jun 2019 11:29:50 +0200 Subject: [PATCH 5/5] Remove unused code --- lib/samly/idp_data.ex | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/lib/samly/idp_data.ex b/lib/samly/idp_data.ex index e0e74f0..96d382f 100644 --- a/lib/samly/idp_data.ex +++ b/lib/samly/idp_data.ex @@ -74,27 +74,16 @@ 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"/#{@entsdesc}/#{@idpdesc}/#{@keydesc}[@use != 'encryption']"l - @cert_selector ~x"/#{@entsdesc}/ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()"s - - defp entity_by_id_selector(id), do: ~x"//#{@entdesc}[@entityID = '#{id}'][1]" # TODO some EntityDescriptor are prefixed with "md:" + defp entity_by_id_selector(id), do: ~x"/#{@entsdesc}/#{@entdesc}[@entityID = '#{id}'][1]" # TODO some EntityDescriptor are prefixed with "md:" + # These functions work on EntityDescriptor element defp sso_redirect_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@redirect}']/@Location"s defp sso_post_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@post}']/@Location"s defp slo_redirect_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@redirect}']/@Location"s defp slo_post_url_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@post}']/@Location"s - - defp nameid_format_selector, do: ~x"/#{@entdesc}/#{@idpdesc}/#{@nameid}[1]/text()"s # TODO for now [1]. - - #defp signing_keys_selector, do: ~x"./ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()[@use != 'encryption']"l defp signing_keys_in_idp_selector, do: ~x"./#{@idpdesc}/#{@keydesc}[@use != 'encryption']"l defp cert_selector, do: ~x"./ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()"s @@ -314,8 +303,8 @@ defmodule Samly.IdpData do 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), + sso_redirect_url: sso_redirect_url, + sso_post_url: sso_post_url, slo_redirect_url: slo_redirect_url, slo_post_url: slo_post_url, nameid_format: nameid_format @@ -476,7 +465,7 @@ defmodule Samly.IdpData do try do SweetXml.xpath(md_xml, selector) rescue - e -> {:error, :entity_not_found} + _ -> {:error, :entity_not_found} end end