Skip to content
Closed
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
5 changes: 5 additions & 0 deletions lib/samly.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
130 changes: 87 additions & 43 deletions lib/samly/idp_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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}} ->
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -373,19 +407,18 @@ 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
md_elem
|> 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()
Expand All @@ -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
Expand All @@ -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