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 diff --git a/lib/samly/idp_data.ex b/lib/samly/idp_data.ex index 0aa662c..96d382f 100644 --- a/lib/samly/idp_data.ex +++ b/lib/samly/idp_data.ex @@ -62,26 +62,30 @@ defmodule Samly.IdpData do valid?: boolean() } - @entdesc "md:EntityDescriptor" - @idpdesc "md:IDPSSODescriptor" + @entsdesc "EntitiesDescriptor" + @entdesc "EntityDescriptor" + @idpdesc "IDPSSODescriptor" @signedreq "WantAuthnRequestsSigned" - @nameid "md:NameIDFormat" - @keydesc "md:KeyDescriptor" - @ssos "md:SingleSignOnService" - @slos "md:SingleLogoutService" + @nameid "NameIDFormat" + @keydesc "KeyDescriptor" + @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" @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 - @cert_selector ~x"./ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()"s + + + 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_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() @@ -121,9 +125,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 +255,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 +265,51 @@ 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 opts[:entity_id] do + nil -> # not provided, we assume file has only one EntityDescriptor + get_entity_id(md_xml) + e -> + e + 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) + 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 + | entity_id: entityID, + signed_requests: get_req_signed(md_xml), + certs: signing_certs, + fingerprints: idp_cert_fingerprints(signing_certs), + 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 + }} + end end # @spec to_esaml_idp_metadata(IdpData.t(), map()) :: :esaml_idp_metadata @@ -364,7 +398,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 @@ -373,11 +407,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 @@ -385,7 +418,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() @@ -395,16 +428,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 +458,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 + + @spec get_entity_descriptor(:xmlElement, entityID :: binary()) :: :xmlElement | nil + defp get_entity_descriptor(md_xml, entityID) do + selector = entity_by_id_selector(entityID) + try do + SweetXml.xpath(md_xml, selector) + rescue + _ -> {:error, :entity_not_found} + end + end + end