diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4060238..ffdd5fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# CHANGELOG
+### v0.8.2
+
++ Handle namespaces in Identity Provider Metadata XML file
+
### v0.8.0
+ Added support for multiple Identity Providers. Check issue: #4.
diff --git a/lib/samly/esaml.ex b/lib/samly/esaml.ex
index f01140e..fe8353a 100644
--- a/lib/samly/esaml.ex
+++ b/lib/samly/esaml.ex
@@ -5,6 +5,8 @@ defmodule Samly.Esaml do
import Record, only: [defrecord: 2, extract: 2]
@esaml_hrl "esaml/include/esaml.hrl"
+ @public_key_hrl "public_key/include/OTP-PUB-KEY.hrl"
+
defrecord :esaml_org, extract(:esaml_org, from_lib: @esaml_hrl)
defrecord :esaml_contact, extract(:esaml_contact, from_lib: @esaml_hrl)
defrecord :esaml_sp_metadata, extract(:esaml_sp_metadata, from_lib: @esaml_hrl)
@@ -16,4 +18,5 @@ defmodule Samly.Esaml do
defrecord :esaml_logoutresp, extract(:esaml_logoutresp, from_lib: @esaml_hrl)
defrecord :esaml_response, extract(:esaml_response, from_lib: @esaml_hrl)
defrecord :esaml_sp, extract(:esaml_sp, from_lib: @esaml_hrl)
+ defrecord :RSAPrivateKey, extract(:RSAPrivateKey, from_lib: @public_key_hrl)
end
diff --git a/lib/samly/idp_data.ex b/lib/samly/idp_data.ex
index eff887a..836cabe 100644
--- a/lib/samly/idp_data.ex
+++ b/lib/samly/idp_data.ex
@@ -1,20 +1,17 @@
defmodule Samly.IdpData do
@moduledoc false
+ import SweetXml
require Logger
require Samly.Esaml
- alias Samly.{ConfigError, Esaml, Helper, IdpData, SpData}
-
- @boolean_attrs [
- :use_redirect_for_req,
- :sign_requests,
- :sign_metadata,
- :signed_assertion_in_resp,
- :signed_envelopes_in_resp
- ]
-
- defstruct id: nil,
- sp_id: nil,
+ alias Samly.{Esaml, Helper, IdpData, SpData}
+
+ @type nameid_formats :: :esaml.name_format()
+ @type certs :: [binary()]
+ @type url :: nil | binary()
+
+ defstruct id: "",
+ sp_id: "",
base_url: nil,
metadata_file: nil,
pre_session_create_pipeline: nil,
@@ -23,148 +20,321 @@ defmodule Samly.IdpData do
sign_metadata: true,
signed_assertion_in_resp: true,
signed_envelopes_in_resp: true,
+ entity_id: "",
+ signed_requests: "",
+ certs: [],
+ sso_redirect_url: nil,
+ sso_post_url: nil,
+ slo_redirect_url: nil,
+ slo_post_url: nil,
+ nameid_format: :unknown,
fingerprints: [],
- esaml_idp_rec: nil,
- esaml_sp_rec: nil
+ esaml_idp_rec: Esaml.esaml_idp_metadata(),
+ esaml_sp_rec: Esaml.esaml_sp(),
+ valid?: false
@type t :: %__MODULE__{
- id: nil | String.t(),
- sp_id: nil | String.t(),
- base_url: nil | String.t(),
- metadata_file: nil | String.t(),
- pre_session_create_pipeline: nil | module,
- use_redirect_for_req: boolean,
- sign_requests: boolean,
- sign_metadata: boolean,
- signed_assertion_in_resp: boolean,
- signed_envelopes_in_resp: boolean,
- fingerprints: keyword(binary),
- esaml_idp_rec: nil | tuple,
- esaml_sp_rec: nil | tuple
+ id: binary(),
+ sp_id: binary(),
+ base_url: nil | binary(),
+ metadata_file: nil | binary(),
+ pre_session_create_pipeline: nil | module(),
+ use_redirect_for_req: boolean(),
+ sign_requests: boolean(),
+ sign_metadata: boolean(),
+ signed_assertion_in_resp: boolean(),
+ signed_envelopes_in_resp: boolean(),
+ entity_id: binary(),
+ signed_requests: binary(),
+ certs: certs(),
+ sso_redirect_url: url(),
+ sso_post_url: url(),
+ slo_redirect_url: url(),
+ slo_post_url: url(),
+ nameid_format: nameid_formats(),
+ fingerprints: [binary()],
+ esaml_idp_rec: :esaml_idp_metadata,
+ esaml_sp_rec: :esaml_sp,
+ valid?: boolean()
}
- @type id :: String.t()
+ @entdesc "md:EntityDescriptor"
+ @idpdesc "md:IDPSSODescriptor"
+ @signedreq "WantAuthnRequestsSigned"
+ @nameid "md:NameIDFormat"
+ @keydesc "md:KeyDescriptor"
+ @ssos "md:SingleSignOnService"
+ @slos "md:SingleLogoutService"
+ @redirect "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ @post "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
- @spec load_identity_providers(list(map), %{required(id) => SpData.t()}, binary) :: %{
- required(id) => __MODULE__.t()
- }
- def load_identity_providers(prov_config, service_providers, base_url) do
+ @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
+
+ @type id :: binary()
+
+ @spec load_providers([map], %{required(id()) => %SpData{}}) ::
+ %{required(id()) => %IdpData{}} | no_return()
+ def load_providers(prov_config, service_providers) do
prov_config
- |> Enum.map(fn idp -> load_idp_data(idp, service_providers, base_url) end)
+ |> Enum.map(fn idp_config -> load_provider(idp_config, service_providers) end)
+ |> Enum.filter(fn idp_data -> idp_data.valid? end)
+ |> Enum.map(fn idp_data -> {idp_data.id, idp_data} end)
|> Enum.into(%{})
end
- @default_idp_metadata_file "idp_metadata.xml"
-
- @spec load_idp_data(map, %{required(id) => SpData.t()}, binary) :: {id, IdpData.t()} | no_return
- defp load_idp_data(%{} = idp_entry, service_providers, default_base_url) do
- with idp_id when idp_id != nil <- Map.get(idp_entry, :id),
- base_url when base_url == nil or is_binary(base_url) <-
- Map.get(idp_entry, :base_url, default_base_url),
- metadata_file when metadata_file != nil <-
- Map.get(idp_entry, :metadata_file, @default_idp_metadata_file),
- pl when pl == nil or is_atom(pl) <- Map.get(idp_entry, :pre_session_create_pipeline),
- {:reading, {:ok, xml}} <- {:reading, File.read(metadata_file)},
- {:parsing, {:ok, mdt}} <- {:parsing, idp_metadata_from_xml(xml)},
- sp_id when sp_id != nil <- Map.get(idp_entry, :sp_id, nil),
- sp when sp != nil <- Map.get(service_providers, sp_id, nil) do
- idp =
- @boolean_attrs
- |> Enum.reduce(%__MODULE__{}, fn attr, idp ->
- v = Map.get(idp_entry, attr)
- if is_boolean(v), do: Map.put(idp, attr, v), else: idp
- end)
-
- idp = %__MODULE__{
- idp
- | id: idp_id,
- sp_id: sp_id,
- base_url: base_url,
- metadata_file: metadata_file,
- pre_session_create_pipeline: pl,
- fingerprints: idp_cert_fingerprints(mdt),
- esaml_idp_rec: mdt
- }
-
- {idp.id, %__MODULE__{idp | esaml_sp_rec: get_esaml_sp_rec(sp, idp, base_url)}}
+ @spec load_provider(map(), %{required(id()) => %SpData{}}) :: %IdpData{} | no_return
+ def load_provider(idp_config, service_providers) do
+ %IdpData{}
+ |> save_idp_config(idp_config)
+ |> load_metadata(idp_config)
+ |> update_esaml_recs(service_providers, idp_config)
+ end
+
+ @spec save_idp_config(%IdpData{}, map()) :: %IdpData{}
+ defp save_idp_config(idp_data, %{id: id, sp_id: sp_id} = opts_map)
+ when is_binary(id) and is_binary(sp_id) do
+ %IdpData{idp_data | id: id, sp_id: sp_id, base_url: Map.get(opts_map, :base_url)}
+ |> set_metadata_file(opts_map)
+ |> set_pipeline(opts_map)
+ |> set_boolean_attr(opts_map, :use_redirect_for_req)
+ |> set_boolean_attr(opts_map, :sign_requests)
+ |> set_boolean_attr(opts_map, :sign_metadata)
+ |> set_boolean_attr(opts_map, :signed_assertion_in_resp)
+ |> set_boolean_attr(opts_map, :signed_envelopes_in_resp)
+ end
+
+ @spec load_metadata(%IdpData{}, map()) :: %IdpData{}
+ 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
+ idp_data
else
{:reading, {:error, reason}} ->
Logger.error("[Samly] Failed to read metadata_file: #{inspect(reason)}")
- raise ConfigError, idp_entry
+ idp_data
{:parsing, {:error, reason}} ->
Logger.error("[Samly] Invalid metadata_file content: #{inspect(reason)}")
- raise ConfigError, idp_entry
+ idp_data
+ end
+ end
+
+ @spec update_esaml_recs(%IdpData{}, %{required(id()) => %SpData{}}, map()) :: %IdpData{}
+ defp update_esaml_recs(idp_data, service_providers, opts_map) do
+ case Map.get(service_providers, idp_data.sp_id) do
+ %SpData{} = sp ->
+ idp_data = %IdpData{idp_data | esaml_idp_rec: to_esaml_idp_metadata(idp_data, opts_map)}
+ idp_data = %IdpData{idp_data | esaml_sp_rec: get_esaml_sp(sp, idp_data)}
+ %IdpData{idp_data | valid?: true}
_ ->
- raise ConfigError, idp_entry
+ Logger.error("[Samly] Unknown/invalid sp_id: #{idp_data.sp_id}")
+ idp_data
end
end
- defp idp_metadata_from_xml(metadata_xml) when is_binary(metadata_xml) do
- try do
- {xml, _} =
- metadata_xml
- |> String.to_charlist()
- |> :xmerl_scan.string(namespace_conformant: true)
+ @default_metadata_file "idp_metadata.xml"
- :esaml.decode_idp_metadata(xml)
- rescue
- _ -> {:error, :invalid_metadata_xml}
- end
+ @spec set_metadata_file(%IdpData{}, map()) :: %IdpData{}
+ defp set_metadata_file(%IdpData{} = idp_data, %{} = opts_map) do
+ %IdpData{idp_data | metadata_file: Map.get(opts_map, :metadata_file, @default_metadata_file)}
+ end
+
+ @spec set_pipeline(%IdpData{}, map()) :: %IdpData{}
+ defp set_pipeline(%IdpData{} = idp_data, %{} = opts_map) do
+ pipeline = Map.get(opts_map, :pre_session_create_pipeline)
+ %IdpData{idp_data | pre_session_create_pipeline: pipeline}
+ end
+
+ @spec set_boolean_attr(%IdpData{}, map(), atom()) :: %IdpData{}
+ defp set_boolean_attr(%IdpData{} = idp_data, %{} = opts_map, attr_name) when is_atom(attr_name) do
+ v = Map.get(opts_map, attr_name)
+ 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
+ xml_opts = [
+ space: :normalize,
+ namespace_conformant: true,
+ comments: false,
+ default_attrs: true
+ ]
+
+ 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)
+ }}
end
- defp idp_cert_fingerprints(idp_metadata) do
- fingerprint =
- idp_metadata
- |> Esaml.esaml_idp_metadata(:certificate)
- |> cert_fingerprint()
- |> String.to_charlist()
+ # @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)
+ sso_url = if sso_url, do: String.to_charlist(sso_url), else: []
+ slo_url = if slo_url, do: String.to_charlist(slo_url), else: :undefined
- [fingerprint] |> :esaml_util.convert_fingerprints()
+ Esaml.esaml_idp_metadata(
+ entity_id: String.to_charlist(idp_data.entity_id),
+ login_location: sso_url,
+ logout_location: slo_url,
+ name_format: idp_data.nameid_format
+ )
+ end
+
+ defp get_sso_slo_urls(%IdpData{} = idp_data, %{use_redirect_for_req: true}) do
+ {idp_data.sso_redirect_url, idp_data.slo_redirect_url}
+ end
+
+ defp get_sso_slo_urls(%IdpData{} = idp_data, %{use_redirect_for_req: false}) do
+ {idp_data.sso_post_url, idp_data.slo_post_url}
+ end
+
+ defp get_sso_slo_urls(%IdpData{} = idp_data, _opts_map) do
+ {
+ idp_data.sso_post_url || idp_data.sso_redirect_url,
+ idp_data.slo_post_url || idp_data.slo_redirect_url
+ }
+ end
+
+ @spec nameid_map(nil | binary) :: nameid_formats()
+ defp nameid_map("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"), do: :email
+ defp nameid_map("urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"), do: :x509
+ defp nameid_map("urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos"), do: :krb
+ defp nameid_map("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"), do: :persistent
+ defp nameid_map("urn:oasis:names:tc:SAML:2.0:nameid-format:transient"), do: :transient
+
+ defp nameid_map("urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName"),
+ do: :windows
+
+ defp nameid_map(_unknown), do: :unknown
+
+ @spec idp_cert_fingerprints(certs()) :: [binary()]
+ defp idp_cert_fingerprints(certs) when is_list(certs) do
+ certs
+ |> Enum.map(&Base.decode64!/1)
+ |> Enum.map(&cert_fingerprint/1)
+ |> Enum.map(&String.to_charlist/1)
+ |> :esaml_util.convert_fingerprints()
end
defp cert_fingerprint(dercert) do
"sha256:" <> (:sha256 |> :crypto.hash(dercert) |> Base.encode64())
end
- def get_esaml_sp_rec(%SpData{} = sp, %IdpData{} = idp, base_url) do
- entity_id =
- case sp.entity_id do
- nil -> :undefined
- :undefined -> :undefined
- id -> String.to_charlist(id)
- end
-
+ # @spec get_esaml_sp(%SpData{}, %IdpData{}) :: :esaml_sp
+ defp get_esaml_sp(%SpData{} = sp_data, %IdpData{} = idp_data) do
idp_id_from = Application.get_env(:samly, :idp_id_from)
- path_segment_idp_id = if idp_id_from == :subdomain, do: nil, else: idp.id
-
- sp_rec =
- Esaml.esaml_sp(
- key: sp.key,
- certificate: sp.cert,
- sp_sign_requests: idp.sign_requests,
- sp_sign_metadata: idp.sign_metadata,
- idp_signs_envelopes: idp.signed_envelopes_in_resp,
- idp_signs_assertions: idp.signed_assertion_in_resp,
- trusted_fingerprints: idp.fingerprints,
- metadata_uri: Helper.get_metadata_uri(base_url, path_segment_idp_id),
- consume_uri: Helper.get_consume_uri(base_url, path_segment_idp_id),
- logout_uri: Helper.get_logout_uri(base_url, path_segment_idp_id),
- entity_id: entity_id,
- org:
- Esaml.esaml_org(
- name: String.to_charlist(sp.org_name),
- displayname: String.to_charlist(sp.org_displayname),
- url: String.to_charlist(sp.org_url)
- ),
- tech:
- Esaml.esaml_contact(
- name: String.to_charlist(sp.contact_name),
- email: String.to_charlist(sp.contact_email)
- )
- )
-
- sp_rec
+ path_segment_idp_id = if idp_id_from == :subdomain, do: nil, else: idp_data.id
+
+ Esaml.esaml_sp(
+ org:
+ Esaml.esaml_org(
+ name: String.to_charlist(sp_data.org_name),
+ displayname: String.to_charlist(sp_data.org_displayname),
+ url: String.to_charlist(sp_data.org_url)
+ ),
+ tech:
+ Esaml.esaml_contact(
+ name: String.to_charlist(sp_data.contact_name),
+ email: String.to_charlist(sp_data.contact_email)
+ ),
+ key: sp_data.key,
+ certificate: sp_data.cert,
+ sp_sign_requests: idp_data.sign_requests,
+ sp_sign_metadata: idp_data.sign_metadata,
+ idp_signs_envelopes: idp_data.signed_envelopes_in_resp,
+ idp_signs_assertions: idp_data.signed_assertion_in_resp,
+ trusted_fingerprints: idp_data.fingerprints,
+ metadata_uri: Helper.get_metadata_uri(idp_data.base_url, path_segment_idp_id),
+ consume_uri: Helper.get_consume_uri(idp_data.base_url, path_segment_idp_id),
+ logout_uri: Helper.get_logout_uri(idp_data.base_url, path_segment_idp_id),
+ entity_id: String.to_charlist(sp_data.entity_id)
+ )
+ end
+
+ @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
+
+ @spec get_nameid_format(:xmlElement) :: nameid_formats()
+ def get_nameid_format(md_elem) do
+ get_data(md_elem, @nameid_format_selector) |> nameid_map()
+ end
+
+ @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_enc_certs(:xmlElement) :: certs()
+ def get_enc_certs(md_elem), do: get_certs(md_elem, @enc_keys_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
+ |> String.split()
+ |> Enum.map(&String.trim/1)
+ |> Enum.join()
+ end)
+ end
+
+ @spec get_sso_redirect_url(:xmlElement) :: url()
+ 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)
+
+ @spec get_slo_redirect_url(:xmlElement) :: url()
+ 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)
+
+ @spec get_url(:xmlElement, %SweetXpath{}) :: url()
+ defp get_url(md_elem, selector) do
+ case get_data(md_elem, selector) do
+ "" -> nil
+ url -> url
+ end
+ end
+
+ @spec get_data(:xmlElement, %SweetXpath{}) :: binary()
+ def get_data(md_elem, selector) do
+ md_elem |> xpath(selector |> add_ns()) |> String.trim()
+ end
+
+ @spec add_ns(%SweetXpath{}) :: %SweetXpath{}
+ defp add_ns(xpath) do
+ xpath
+ |> 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
diff --git a/lib/samly/provider.ex b/lib/samly/provider.ex
index a588dcd..133a0b3 100644
--- a/lib/samly/provider.ex
+++ b/lib/samly/provider.ex
@@ -53,14 +53,10 @@ defmodule Samly.Provider do
Application.put_env(:samly, :idp_id_from, idp_id_from)
- service_providers = Samly.SpData.load_service_providers(opts[:service_providers] || [])
+ service_providers = Samly.SpData.load_providers(opts[:service_providers] || [])
identity_providers =
- Samly.IdpData.load_identity_providers(
- opts[:identity_providers] || [],
- service_providers,
- opts[:base_url]
- )
+ Samly.IdpData.load_providers(opts[:identity_providers] || [], service_providers)
Application.put_env(:samly, :service_providers, service_providers)
Application.put_env(:samly, :identity_providers, identity_providers)
diff --git a/lib/samly/sp_data.ex b/lib/samly/sp_data.ex
index 2947951..cde934f 100644
--- a/lib/samly/sp_data.ex
+++ b/lib/samly/sp_data.ex
@@ -2,35 +2,38 @@ defmodule Samly.SpData do
@moduledoc false
require Logger
- alias Samly.ConfigError
+ require Samly.Esaml
+ alias Samly.SpData
- defstruct id: nil,
- entity_id: :undefined,
- certfile: nil,
- keyfile: nil,
- contact_name: nil,
- contact_email: nil,
- org_name: nil,
- org_displayname: nil,
- org_url: nil,
- cert: nil,
- key: nil
+ defstruct id: "",
+ entity_id: "",
+ certfile: "",
+ keyfile: "",
+ contact_name: "",
+ contact_email: "",
+ org_name: "",
+ org_displayname: "",
+ org_url: "",
+ cert: :undefined,
+ key: :undefined,
+ valid?: true
@type t :: %__MODULE__{
- id: nil | String.t(),
- entity_id: nil | :undefined | String.t(),
- certfile: nil | String.t(),
- keyfile: nil | String.t(),
- contact_name: nil | String.t(),
- contact_email: nil | String.t(),
- org_name: nil | String.t(),
- org_displayname: nil | String.t(),
- org_url: nil | String.t(),
- cert: nil | binary,
- key: nil | tuple
+ id: binary(),
+ entity_id: binary(),
+ certfile: binary(),
+ keyfile: binary(),
+ contact_name: binary(),
+ contact_email: binary(),
+ org_name: binary(),
+ org_displayname: binary(),
+ org_url: binary(),
+ cert: :undefined | binary(),
+ key: :undefined | :RSAPrivateKey,
+ valid?: boolean()
}
- @type id :: String.t()
+ @type id :: binary
@default_contact_name "Samly SP Admin"
@default_contact_email "admin@samly"
@@ -38,53 +41,65 @@ defmodule Samly.SpData do
@default_org_displayname "SAML SP built with Samly"
@default_org_url "https://github.com/handnot2/samly"
- @spec load_service_providers(list(map)) :: %{required(id) => t}
- def load_service_providers(providers) do
- providers
- |> Enum.map(&load_sp/1)
+ @spec load_providers(list(map)) :: %{required(id) => t}
+ def load_providers(prov_configs) do
+ prov_configs
+ |> Enum.map(&load_provider/1)
+ |> Enum.filter(fn sp_data -> sp_data.valid? end)
+ |> Enum.map(fn sp_data -> {sp_data.id, sp_data} end)
|> Enum.into(%{})
end
- @spec load_sp(map) :: {String.t(), t} | no_return
- defp load_sp(%{} = provider) do
- sp = %__MODULE__{
- id: Map.get(provider, :id, nil),
- entity_id: Map.get(provider, :entity_id, nil),
- certfile: Map.get(provider, :certfile, nil),
- keyfile: Map.get(provider, :keyfile, nil),
- contact_name: Map.get(provider, :contact_name, @default_contact_name),
- contact_email: Map.get(provider, :contact_email, @default_contact_email),
- org_name: Map.get(provider, :org_name, @default_org_name),
- org_displayname: Map.get(provider, :org_displayname, @default_org_displayname),
- org_url: Map.get(provider, :org_url, @default_org_url)
+ @spec load_provider(map) :: %SpData{} | no_return
+ def load_provider(%{} = opts_map) do
+ sp_data = %__MODULE__{
+ id: Map.get(opts_map, :id, ""),
+ entity_id: Map.get(opts_map, :entity_id, ""),
+ certfile: Map.get(opts_map, :certfile, ""),
+ keyfile: Map.get(opts_map, :keyfile, ""),
+ contact_name: Map.get(opts_map, :contact_name, @default_contact_name),
+ contact_email: Map.get(opts_map, :contact_email, @default_contact_email),
+ org_name: Map.get(opts_map, :org_name, @default_org_name),
+ org_displayname: Map.get(opts_map, :org_displayname, @default_org_displayname),
+ org_url: Map.get(opts_map, :org_url, @default_org_url)
}
- sp = %__MODULE__{sp | cert: load_cert(sp.certfile, sp.id), key: load_key(sp.keyfile, sp.id)}
+ sp_data |> set_id(opts_map) |> load_cert(opts_map) |> load_key(opts_map)
+ end
- if sp.id == nil || sp.certfile == nil || sp.keyfile == nil do
- raise ConfigError, provider
- end
+ @spec set_id(%SpData{}, map()) :: %SpData{}
+ defp set_id(%SpData{} = sp_data, %{} = opts_map) do
+ case Map.get(opts_map, :id, "") do
+ "" ->
+ Logger.error("[Samly] Invalid SP Config: #{inspect(opts_map)}")
+ %SpData{sp_data | valid?: false}
- {sp.id, sp}
+ id ->
+ %SpData{sp_data | id: id}
+ end
end
- defp load_key(file, label) do
+ @spec load_cert(%SpData{}, map()) :: %SpData{}
+ defp load_cert(%SpData{certfile: certfile} = sp_data, %{} = opts_map) do
try do
- file |> :esaml_util.load_private_key()
+ cert = :esaml_util.load_certificate(certfile)
+ %SpData{sp_data | cert: cert}
rescue
_error ->
- Logger.error("[Samly] Failed load SP keyfile: #{label}:#{file}")
- nil
+ Logger.error("[Samly] Failed load SP certfile: #{inspect(opts_map)}")
+ %SpData{sp_data | valid?: false}
end
end
- defp load_cert(file, label) do
+ @spec load_key(%SpData{}, map()) :: %SpData{}
+ defp load_key(%SpData{keyfile: keyfile} = sp_data, %{} = opts_map) do
try do
- file |> :esaml_util.load_certificate()
+ key = :esaml_util.load_private_key(keyfile)
+ %SpData{sp_data | key: key}
rescue
_error ->
- Logger.error("[Samly] Failed load SP certfile: #{label}:#{file}")
- nil
+ Logger.error("[Samly] Failed load SP keyfile: #{inspect(opts_map)}")
+ %SpData{sp_data | key: :undefined, valid?: false}
end
end
end
diff --git a/mix.exs b/mix.exs
index 71a00f1..e986a42 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,7 +1,7 @@
defmodule Samly.Mixfile do
use Mix.Project
- @version "0.8.1"
+ @version "0.8.2"
@description "SAML SP SSO made easy"
@source_url "https://github.com/handnot2/samly"
@@ -30,6 +30,7 @@ defmodule Samly.Mixfile do
[
{:plug, "~> 1.4"},
{:esaml, "~> 3.1"},
+ {:sweet_xml, "~> 0.6"},
{:ex_doc, "~> 0.18", only: :dev},
{:inch_ex, "~> 0.5", only: :docs}
]
diff --git a/mix.lock b/mix.lock
index 41dec00..68f46f4 100644
--- a/mix.lock
+++ b/mix.lock
@@ -7,4 +7,5 @@
"mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
- "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}}
+ "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
+ "sweet_xml": {:hex, :sweet_xml, "0.6.5", "dd9cde443212b505d1b5f9758feb2000e66a14d3c449f04c572f3048c66e6697", [:mix], [], "hexpm"}}
diff --git a/test/data/azure_fed_metadata.xml b/test/data/azure_fed_metadata.xml
new file mode 100644
index 0000000..0bc9549
--- /dev/null
+++ b/test/data/azure_fed_metadata.xml
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ PxBXxVXHYmtelxnhr2ewHTGKEkuviPF429grQa1WNIw=
+
+
+ o/8AXiWeX8TpD76ix+hlpX//CnBjL4i5xxniJLDTqcKsVBCa544vBOJsCFKS4Syd7AoXD5Yhj6PPzpfUq6kR+vQRZS1w5/XQ7juSv/GCSLSJaL3Zhhm2Uej/jtJf+Wxq8yrsTXyECPfqx8XTD6RIbEOGcn9Ug3wkuUpnzf3Kgl8PNUNhYa+bKyehsUNmPr5geFqxk/o1Pq/prVFy7gBGXEGPMHigkczYya2vxhZCpLkKUcqm3JlQvPdCFuH060F5/MSKxEJttt/KQIer7fvLD832Yw1LeI+aarJxOXEAgJpbohlE5Wzxs2GvrSvzCqiECTCXZOVAdQ+LGIHJMvehcA==
+
+
+ MIIDBTCCAe2gAwIBAgIQQm0sN9lDrblM/7U/vYMVmTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE3MDkwNjAwMDAwMFoXDTE5MDkwNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAONDCuIodN3I6J7+Wc9j5sY5MgLkGZZuUxLApHEmIhgWIVSSyMgM+eu0Nrr5FuIylJrKg/SHOJeLwFuYZOEMfRUVcKRewUTIBKmILqlg5+d56ztYZATuhm60B5bp1RUlUQMZ0GTepz9xpS9hO4Mv5GRBxR5xoBKCOF74xfVuHNkk/wnbV/Yggwt82FrGm8CpiXjbmFzy61Vmp6KbFDadJDij2mmICSwzAJEoVvklLzcL/4Vf03v3l8aBkI/SPfDjm6k55dqeDm+nEIN/baWbs7M2WtNkJXNePy8dR0GkdlhbgESEIJdSVLWeBFt8eV0JQqUXcPCjhwpJE89jrHhwyCECAwEAAaMhMB8wHQYDVR0OBBYEFNISA3dtAzEd0muqNDbWm3kvNlJDMA0GCSqGSIb3DQEBCwUAA4IBAQClLLoAvg3dYqWO63Z6O5L7yataGcilmL3YUqCFoRKsuwej2T833qyc1iLG0iWCGeWAUonKXuGwfCSSSj2E3ksLtgV6xmuMl+NuVPpRpQo+38n+OxUoWKu963dMxnORFENEqKW0pMioipMk/HBaW3aJWyH1oT2rZ3KhFm67SFjKscF8ShAE82tQQIFwEFAXjMItW2oZVGDz3vDOaJN5xC8rfA6xkXTdcCuzy74SalKkLhpBO8S3XIOBVRZw+l0Koog8YNqhsvGsGS+hGXXNlCZTg0I1tR3g2DcSuHRcuTZKh7Z7XPPsDgleNirtvYFEvdvD4K2I7gb2H1xQn87oYAIX
+
+
+
+
+
+
+
+
+ MIIDBTCCAe2gAwIBAgIQQm0sN9lDrblM/7U/vYMVmTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE3MDkwNjAwMDAwMFoXDTE5MDkwNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAONDCuIodN3I6J7+Wc9j5sY5MgLkGZZuUxLApHEmIhgWIVSSyMgM+eu0Nrr5FuIylJrKg/SHOJeLwFuYZOEMfRUVcKRewUTIBKmILqlg5+d56ztYZATuhm60B5bp1RUlUQMZ0GTepz9xpS9hO4Mv5GRBxR5xoBKCOF74xfVuHNkk/wnbV/Yggwt82FrGm8CpiXjbmFzy61Vmp6KbFDadJDij2mmICSwzAJEoVvklLzcL/4Vf03v3l8aBkI/SPfDjm6k55dqeDm+nEIN/baWbs7M2WtNkJXNePy8dR0GkdlhbgESEIJdSVLWeBFt8eV0JQqUXcPCjhwpJE89jrHhwyCECAwEAAaMhMB8wHQYDVR0OBBYEFNISA3dtAzEd0muqNDbWm3kvNlJDMA0GCSqGSIb3DQEBCwUAA4IBAQClLLoAvg3dYqWO63Z6O5L7yataGcilmL3YUqCFoRKsuwej2T833qyc1iLG0iWCGeWAUonKXuGwfCSSSj2E3ksLtgV6xmuMl+NuVPpRpQo+38n+OxUoWKu963dMxnORFENEqKW0pMioipMk/HBaW3aJWyH1oT2rZ3KhFm67SFjKscF8ShAE82tQQIFwEFAXjMItW2oZVGDz3vDOaJN5xC8rfA6xkXTdcCuzy74SalKkLhpBO8S3XIOBVRZw+l0Koog8YNqhsvGsGS+hGXXNlCZTg0I1tR3g2DcSuHRcuTZKh7Z7XPPsDgleNirtvYFEvdvD4K2I7gb2H1xQn87oYAIX
+
+
+
+
+
+
+
+ MIIDBTCCAe2gAwIBAgIQG3bMDDyO6q1GrI5sdZXCrTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE3MTAxODAwMDAwMFoXDTE5MTAxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMPkg1ILQtpwFPPqiBBV3wXd89KoDW5NA/DiBQPZYINe//g6gdrOcnT6JjS5mnntXURsvlC8a8QPNCeX9zuxi0MvEoXjuyROJwnAhZyHmkWQh850pUiFTICCuDJXBrMiMdLYfeaJ9C0SApw2OhDiCED77Hh3oxuP49ooS5My96/b3PM76KC/GTv4VM5ydPOXpvUpLzMoavQdOGUCjUYU2z9twCYXmCLtwOiNWFW9yqof6tSAxEZRezq0Hi9qU6DF0MIp0TT6FKvUC8ui4pcYX7hZ6U9yOXJVB0+6HXdpGQ42sypvoK1h8mcRRLA5Ad/ox6YIJ39j0tJm5y+4H37NvHUCAwEAAaMhMB8wHQYDVR0OBBYEFCO/QGygHvo1YiKeQVulJFVxO9dnMA0GCSqGSIb3DQEBCwUAA4IBAQBZTJK52b+QnBbLicaT5uxC3JnRwps6RovQzPZRBLpxATq4kj5jNMhegb5fx4Rc1dpepXWJHAGzD0Nwsab/vYSx7iqyU02IAUkwt3k7XyYK17R6gTgUAxEFBfRKM3PSFiH0b3tGA+baLT3BdY5U6ZqjxhFA0Rh7tzPZM1TO2WtENk3hKmG5r5GKECnwa5NiE5jxN+d6i8dqM+vMqDvIrfqTA3ooQWXpvs0I9YUWl/LjBNFqyY3rMzxLX3STobLFf8ayHIvVmtiFSM3glCO+8UtGKLwNnPFIfYx3VstJjOO8rjP0Z/oaZwhD0A7MrNp4ztwmXAIzYkGTVyDsNuQJgi1e
+
+
+
+
+
+
+
+ MIIDKDCCAhCgAwIBAgIQBHJvVNxP1oZO4HYKh+rypDANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMTYxMTE2MDgwMDAwWhcNMTgxMTE2MDgwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChn5BCs24Hh6L0BNitPrV5s+2/DBhaeytOmnghJKnqeJlhv3ZczShRM2Cp38LW8Y3wn7L3AJtolaSkF/joKN1l6GupzM+HOEdq7xZxFehxIHW7+25mG/WigBnhsBzLv1SR4uIbrQeS5M0kkLwJ9pOnVH3uzMGG6TRXPnK3ivlKl97AiUEKdlRjCQNLXvYf1ZqlC77c/ZCOHSX4kvIKR2uG+LNlSTRq2rn8AgMpFT4DSlEZz4RmFQvQupQzPpzozaz/gadBpJy/jgDmJlQMPXkHp7wClvbIBGiGRaY6eZFxNV96zwSR/GPNkTObdw2S8/SiAgvIhIcqWTPLY6aVTqJfAgMBAAGjWDBWMFQGA1UdAQRNMEuAEDUj0BrjP0RTbmoRPTRMY3WhJTAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXOCEARyb1TcT9aGTuB2Cofq8qQwDQYJKoZIhvcNAQELBQADggEBAGnLhDHVz2gLDiu9L34V3ro/6xZDiSWhGyHcGqky7UlzQH3pT5so8iF5P0WzYqVtogPsyC2LPJYSTt2vmQugD4xlu/wbvMFLcV0hmNoTKCF1QTVtEQiAiy0Aq+eoF7Al5fV1S3Sune0uQHimuUFHCmUuF190MLcHcdWnPAmzIc8fv7quRUUsExXmxSX2ktUYQXzqFyIOSnDCuWFm6tpfK5JXS8fW5bpqTlrysXXz/OW/8NFGq/alfjrya4ojrOYLpunGriEtNPwK7hxj1AlCYEWaRHRXaUIW1ByoSff/6Y6+ZhXPUe0cDlNRt/qIz5aflwO7+W8baTS4O8m/icu7ItE=
+
+
+
+
+
+
+ Name
+ The mutable display name of the user.
+
+
+ Subject
+ An immutable, globally unique, non-reusable identifier of the user that is unique to the application for which a token is issued.Given Name
+ First name of the user.
+
+
+ Surname
+ Last name of the user.
+
+
+ Display Name
+ Display name of the user.
+
+
+ Nick Name
+ Nick name of the user.
+
+
+ Authentication Instant
+ The time (UTC) when the user is authenticated to Windows Azure Active Directory.
+
+
+ Authentication Method
+ The method that Windows Azure Active Directory uses to authenticate users.
+
+
+ ObjectIdentifier
+ Primary identifier for the user in the directory. Immutable, globally unique, non-reusable.
+
+
+ TenantId
+ Identifier for the user's tenant.
+
+
+ IdentityProvider
+ Identity provider for the user.
+
+
+ Email
+ Email address of the user.
+
+
+ Groups
+ Groups of the user.
+
+
+ External Access Token
+ Access token issued by external identity provider.
+
+
+ External Access Token Expiration
+ UTC expiration time of access token issued by external identity provider.
+
+
+ External OpenID 2.0 Identifier
+ OpenID 2.0 identifier issued by external identity provider.
+
+
+ GroupsOverageClaim
+ Issued when number of user's group claims exceeds return limit.
+
+
+ Role Claim
+ Roles that the user or Service Principal is attached to
+
+
+ RoleTemplate Id Claim
+ Role template id of the Built-in Directory Roles that the user is a member of
+
+
+
+
+ https://login.microsoftonline.com/common/wsfed
+
+
+
+
+ https://login.microsoftonline.com/common/wsfed
+
+
+
+ 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https://sts.windows.net/%7Btenantid%7D/https://login.microsoftonline.com/common/wsfedhttps://login.microsoftonline.com/common/wsfed
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
diff --git a/test/data/onelogin_idp_metadata.xml b/test/data/onelogin_idp_metadata.xml
new file mode 100644
index 0000000..c23cae6
--- /dev/null
+++ b/test/data/onelogin_idp_metadata.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+MIIDEDCCAfigAwIBAgIVALIKvj2cp9VIRuWNKjHwGiV1ITxfMA0GCSqGSIb3DQEB
+CwUAMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDAeFw0xNzExMDcxNTE3NDNaFw0zNzEx
+MDcxNTE3NDNaMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBALmLey0ZWrMYz2O+CTTjr97UcDkaaUzbIfTjw2/0HofU
+czVl5b3ElzOjnB0pJ6xl8s27Qyctdq0EZrlmR9hHKUnF2U9v95rG005Cx+QQVdsg
+OaZjDZsmC7eLABQcLdfLP3f42dOozxCH9bBQcs+f/ndrumxdL2sIXflmer4mXfEg
+57+HCRL3+s9y07fxdi2LB2ac5gVI8HJbIo8bPOeCyWLYc3UpSZUsxTZouK/wjft0
+TMNJ0gu5TCptiyxxZRhJcg6gm2L77d6rjbnax8fWqj/YNMlXkT7BagUxbPbEklAY
+YzIKnt6egw8SpOURgAJynZDl4cYxM1QynfIuWaYi5gECAwEAAaNZMFcwHQYDVR0O
+BBYEFJhsexKytkNruELg386zOyW1icH1MDYGA1UdEQQvMC2CCXNhbWx5LmlkcIYg
+aHR0cHM6Ly9zYW1seS5pZHAvaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQAD
+ggEBAAhCAuNPhWsrd/b3MSRK+I5GGe0eDSkpQiCT0ULbqucW+BHj0by0DOy6yP98
+0mfATI6eDJ/LUpT+Wenxljujy5nh0EPu6t6RG/MvWTplnr0//m+41L8tQXEZtZkM
+NKkrFieiUBe+rcDx7xywzGUvM0qWRdTyD7Yb0JUU8bZKnIFAEZ+mm8Fu1KfXI6kK
+sdeh/6gdpah9v2mermegdNfGpktWtXOukvmR4M8ZYLEyAwGQQAuqJcUnOUwuVMFU
+chLUXbAfJUduUkGQ3WKw/SNKyv/7Z2ayr7wlkEA7fxIIrLaJzSm928y9wB9s7Irr
+78rpJG67hSRlA+CGTGZyksrk2fE=
+
+
+
+
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+
+
+
+
+
+
diff --git a/test/data/shibboleth_idp_metadata.xml b/test/data/shibboleth_idp_metadata.xml
new file mode 100644
index 0000000..f92c26f
--- /dev/null
+++ b/test/data/shibboleth_idp_metadata.xml
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+ example.org
+
+
+
+
+
+
+
+MIIDEDCCAfigAwIBAgIVALIKvj2cp9VIRuWNKjHwGiV1ITxfMA0GCSqGSIb3DQEB
+CwUAMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDAeFw0xNzExMDcxNTE3NDNaFw0zNzEx
+MDcxNTE3NDNaMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBALmLey0ZWrMYz2O+CTTjr97UcDkaaUzbIfTjw2/0HofU
+czVl5b3ElzOjnB0pJ6xl8s27Qyctdq0EZrlmR9hHKUnF2U9v95rG005Cx+QQVdsg
+OaZjDZsmC7eLABQcLdfLP3f42dOozxCH9bBQcs+f/ndrumxdL2sIXflmer4mXfEg
+57+HCRL3+s9y07fxdi2LB2ac5gVI8HJbIo8bPOeCyWLYc3UpSZUsxTZouK/wjft0
+TMNJ0gu5TCptiyxxZRhJcg6gm2L77d6rjbnax8fWqj/YNMlXkT7BagUxbPbEklAY
+YzIKnt6egw8SpOURgAJynZDl4cYxM1QynfIuWaYi5gECAwEAAaNZMFcwHQYDVR0O
+BBYEFJhsexKytkNruELg386zOyW1icH1MDYGA1UdEQQvMC2CCXNhbWx5LmlkcIYg
+aHR0cHM6Ly9zYW1seS5pZHAvaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQAD
+ggEBAAhCAuNPhWsrd/b3MSRK+I5GGe0eDSkpQiCT0ULbqucW+BHj0by0DOy6yP98
+0mfATI6eDJ/LUpT+Wenxljujy5nh0EPu6t6RG/MvWTplnr0//m+41L8tQXEZtZkM
+NKkrFieiUBe+rcDx7xywzGUvM0qWRdTyD7Yb0JUU8bZKnIFAEZ+mm8Fu1KfXI6kK
+sdeh/6gdpah9v2mermegdNfGpktWtXOukvmR4M8ZYLEyAwGQQAuqJcUnOUwuVMFU
+chLUXbAfJUduUkGQ3WKw/SNKyv/7Z2ayr7wlkEA7fxIIrLaJzSm928y9wB9s7Irr
+78rpJG67hSRlA+CGTGZyksrk2fE=
+
+
+
+
+
+
+
+
+
+MIIDDzCCAfegAwIBAgIUawrhfDAK6t2xrB0CCKKiLILXdUEwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJc2FtbHkuaWRwMB4XDTE3MTEwNzE1MTczMVoXDTM3MTEw
+NzE1MTczMVowFDESMBAGA1UEAwwJc2FtbHkuaWRwMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAgx/bOpdbzlR33T9ZgVkLWAYVTJvPnS6EbOouV9iSsrul
+9Sg6kxrb3NK9HumFqalDxmwZH81snt+isgIUyIX0uZDEu0eBt++hGrLH4/gvZjQW
+Ow5ju1+dVOIt28Qy9+ExzWS4XEblId4m8xxNew2FlKKQwThYojuGH95FkMDo736A
+wLJNol7FY3BgZwcGajIDFQoAyBhUrfrScBvE/eEGmPyJPzIO7NmtrlPq5NmATi4W
+fG5U7I+dT6t3rasPbhbKf1xsN5dNOgHEYAZmp+wqMJ9t4GNDJgqt5Sftryd/zskk
+9fPjk8MFll4XVJ9NGjg1AjUwS3swQBIK2xejK1zl2QIDAQABo1kwVzAdBgNVHQ4E
+FgQUs0m5/0iOU8Z9Rf7JrbfYd2EUrBowNgYDVR0RBC8wLYIJc2FtbHkuaWRwhiBo
+dHRwczovL3NhbWx5LmlkcC9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsFAAOC
+AQEAYMBZvg8V9468Jn1mbMJ7YuOb1A8XFB5nuewpBzjnFoDZKRsUim6DUOAt/NYZ
+xWxaC7l8t70LdGskxaFgdE2+L7z7TibZRj2Ibc+CRg20O615rCR3C5fUdRv6Z4C/
+pSu5yNPQz5NPWOI5ryFmbp9TCf+Yh8hwa49BY/pOIPSjGk5usJk9OVBSqwdJrBpp
+iO9wLLCB2z6ZFpK3LpF2DpGAewuJOzHaD8VfPoqqAcXnWR+Q263QOrfv+9GeFv8G
+odjFk9wMTYRX5iitBAank4vrE0USbovz30F+wK4VLxm/806Evh4wOPwkroxBomnc
+a/dmaqTz0EZ80cr5Le+54VhF/w==
+
+
+
+
+
+
+
+
+
+MIIDDzCCAfegAwIBAgIUFjdO5mF6khL3fHOV3CcPpMD1juMwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJc2FtbHkuaWRwMB4XDTE3MTEwNzE1MTczMloXDTM3MTEw
+NzE1MTczMlowFDESMBAGA1UEAwwJc2FtbHkuaWRwMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA8X53oxFAwsIdsCltzcko0NgUg63NQtOxXVEoqmPpSKxX
+8wvsXWa0/KLa5XOWuoxshc45yI0Tn9bxGxrzP90StABRIKSnbjs2k1sebXPbOHDM
+LPMyMcKq34qiIffg+KfPHiOajksB8gD1EPdu8hSTPuLsiJnCUrWtPB8s/IydEdTa
+o79Hk0cd8BQskB/l7PwuakPdsF6QRJj+kcFrBAkECzrET0LZahpIPKmSQpE3LQ08
+XxMLEoFJrhvt1Dwj4LTEbmjrJY+l+2HpJxXH9ibwVlJpTzqNAz81vC7Xad/c4oMg
+thnk1uhk73IqmeAgQA8FKFKCLwK7iFNJ8X/B99EfYwIDAQABo1kwVzAdBgNVHQ4E
+FgQU3jbqNvR4yPzb9R173XFB+E+VGCIwNgYDVR0RBC8wLYIJc2FtbHkuaWRwhiBo
+dHRwczovL3NhbWx5LmlkcC9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsFAAOC
+AQEAHlcb/jlVDu8Lj4X6u/IAEiBzwtVjVLpH6X0GHWq1WU8IN4zAtPhsc4ELmtGj
+ZJNCZwD7eBi4Ibby6I3NCwvhafpRVB8+PCD/jGB53t/vV8VZII/Zpk/wgYTh11T+
+TyOuW/JCckLeHLSwYPiDHmk3z9e8K9CVI/lhHvNXoEEfGtrIiGE8T00RhBAWvflv
+2YATb9dsUGKXojAKi1xZpBToxQPllryjGqN+Big6IcR0Pv/IVOxVvcgU5ZOtYF0g
+HuK+YEeTSVMkW24+aC6185LocIKrUPg9d/kyUTi8WK047fctGv+7IdP5mbeGMRiU
+pgvjU5nUCkPOUOyZZiVO0VMEIA==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ example.org
+
+
+
+
+
+
+MIIDEDCCAfigAwIBAgIVALIKvj2cp9VIRuWNKjHwGiV1ITxfMA0GCSqGSIb3DQEB
+CwUAMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDAeFw0xNzExMDcxNTE3NDNaFw0zNzEx
+MDcxNTE3NDNaMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBALmLey0ZWrMYz2O+CTTjr97UcDkaaUzbIfTjw2/0HofU
+czVl5b3ElzOjnB0pJ6xl8s27Qyctdq0EZrlmR9hHKUnF2U9v95rG005Cx+QQVdsg
+OaZjDZsmC7eLABQcLdfLP3f42dOozxCH9bBQcs+f/ndrumxdL2sIXflmer4mXfEg
+57+HCRL3+s9y07fxdi2LB2ac5gVI8HJbIo8bPOeCyWLYc3UpSZUsxTZouK/wjft0
+TMNJ0gu5TCptiyxxZRhJcg6gm2L77d6rjbnax8fWqj/YNMlXkT7BagUxbPbEklAY
+YzIKnt6egw8SpOURgAJynZDl4cYxM1QynfIuWaYi5gECAwEAAaNZMFcwHQYDVR0O
+BBYEFJhsexKytkNruELg386zOyW1icH1MDYGA1UdEQQvMC2CCXNhbWx5LmlkcIYg
+aHR0cHM6Ly9zYW1seS5pZHAvaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQAD
+ggEBAAhCAuNPhWsrd/b3MSRK+I5GGe0eDSkpQiCT0ULbqucW+BHj0by0DOy6yP98
+0mfATI6eDJ/LUpT+Wenxljujy5nh0EPu6t6RG/MvWTplnr0//m+41L8tQXEZtZkM
+NKkrFieiUBe+rcDx7xywzGUvM0qWRdTyD7Yb0JUU8bZKnIFAEZ+mm8Fu1KfXI6kK
+sdeh/6gdpah9v2mermegdNfGpktWtXOukvmR4M8ZYLEyAwGQQAuqJcUnOUwuVMFU
+chLUXbAfJUduUkGQ3WKw/SNKyv/7Z2ayr7wlkEA7fxIIrLaJzSm928y9wB9s7Irr
+78rpJG67hSRlA+CGTGZyksrk2fE=
+
+
+
+
+
+
+
+
+
+MIIDDzCCAfegAwIBAgIUawrhfDAK6t2xrB0CCKKiLILXdUEwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJc2FtbHkuaWRwMB4XDTE3MTEwNzE1MTczMVoXDTM3MTEw
+NzE1MTczMVowFDESMBAGA1UEAwwJc2FtbHkuaWRwMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAgx/bOpdbzlR33T9ZgVkLWAYVTJvPnS6EbOouV9iSsrul
+9Sg6kxrb3NK9HumFqalDxmwZH81snt+isgIUyIX0uZDEu0eBt++hGrLH4/gvZjQW
+Ow5ju1+dVOIt28Qy9+ExzWS4XEblId4m8xxNew2FlKKQwThYojuGH95FkMDo736A
+wLJNol7FY3BgZwcGajIDFQoAyBhUrfrScBvE/eEGmPyJPzIO7NmtrlPq5NmATi4W
+fG5U7I+dT6t3rasPbhbKf1xsN5dNOgHEYAZmp+wqMJ9t4GNDJgqt5Sftryd/zskk
+9fPjk8MFll4XVJ9NGjg1AjUwS3swQBIK2xejK1zl2QIDAQABo1kwVzAdBgNVHQ4E
+FgQUs0m5/0iOU8Z9Rf7JrbfYd2EUrBowNgYDVR0RBC8wLYIJc2FtbHkuaWRwhiBo
+dHRwczovL3NhbWx5LmlkcC9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsFAAOC
+AQEAYMBZvg8V9468Jn1mbMJ7YuOb1A8XFB5nuewpBzjnFoDZKRsUim6DUOAt/NYZ
+xWxaC7l8t70LdGskxaFgdE2+L7z7TibZRj2Ibc+CRg20O615rCR3C5fUdRv6Z4C/
+pSu5yNPQz5NPWOI5ryFmbp9TCf+Yh8hwa49BY/pOIPSjGk5usJk9OVBSqwdJrBpp
+iO9wLLCB2z6ZFpK3LpF2DpGAewuJOzHaD8VfPoqqAcXnWR+Q263QOrfv+9GeFv8G
+odjFk9wMTYRX5iitBAank4vrE0USbovz30F+wK4VLxm/806Evh4wOPwkroxBomnc
+a/dmaqTz0EZ80cr5Le+54VhF/w==
+
+
+
+
+
+
+
+
+
+MIIDDzCCAfegAwIBAgIUFjdO5mF6khL3fHOV3CcPpMD1juMwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJc2FtbHkuaWRwMB4XDTE3MTEwNzE1MTczMloXDTM3MTEw
+NzE1MTczMlowFDESMBAGA1UEAwwJc2FtbHkuaWRwMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA8X53oxFAwsIdsCltzcko0NgUg63NQtOxXVEoqmPpSKxX
+8wvsXWa0/KLa5XOWuoxshc45yI0Tn9bxGxrzP90StABRIKSnbjs2k1sebXPbOHDM
+LPMyMcKq34qiIffg+KfPHiOajksB8gD1EPdu8hSTPuLsiJnCUrWtPB8s/IydEdTa
+o79Hk0cd8BQskB/l7PwuakPdsF6QRJj+kcFrBAkECzrET0LZahpIPKmSQpE3LQ08
+XxMLEoFJrhvt1Dwj4LTEbmjrJY+l+2HpJxXH9ibwVlJpTzqNAz81vC7Xad/c4oMg
+thnk1uhk73IqmeAgQA8FKFKCLwK7iFNJ8X/B99EfYwIDAQABo1kwVzAdBgNVHQ4E
+FgQU3jbqNvR4yPzb9R173XFB+E+VGCIwNgYDVR0RBC8wLYIJc2FtbHkuaWRwhiBo
+dHRwczovL3NhbWx5LmlkcC9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsFAAOC
+AQEAHlcb/jlVDu8Lj4X6u/IAEiBzwtVjVLpH6X0GHWq1WU8IN4zAtPhsc4ELmtGj
+ZJNCZwD7eBi4Ibby6I3NCwvhafpRVB8+PCD/jGB53t/vV8VZII/Zpk/wgYTh11T+
+TyOuW/JCckLeHLSwYPiDHmk3z9e8K9CVI/lhHvNXoEEfGtrIiGE8T00RhBAWvflv
+2YATb9dsUGKXojAKi1xZpBToxQPllryjGqN+Big6IcR0Pv/IVOxVvcgU5ZOtYF0g
+HuK+YEeTSVMkW24+aC6185LocIKrUPg9d/kyUTi8WK047fctGv+7IdP5mbeGMRiU
+pgvjU5nUCkPOUOyZZiVO0VMEIA==
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/data/simplesaml_idp_metadata.xml b/test/data/simplesaml_idp_metadata.xml
new file mode 100644
index 0000000..7eb081e
--- /dev/null
+++ b/test/data/simplesaml_idp_metadata.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ MIIF1TCCA72gAwIBAgIJAKib45YfneRFMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYDVQQGEwJVUzERMA8GA1UECAwITWlkbGFuZHMxEjAQBgNVBAcMCVNhZmV2aWxsZTEWMBQGA1UECgwNSUQgRmVkZXJhdGlvbjEhMB8GA1UECwwYRGVwYXJ0bWVudCBvZiBJZGVudGl0aWVzMQ8wDQYDVQQDDAZteS5pZHAwHhcNMTcwOTI5MjAyNDAxWhcNMTgwOTI5MjAyNDAxWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pZGxhbmRzMRIwEAYDVQQHDAlTYWZldmlsbGUxFjAUBgNVBAoMDUlEIEZlZGVyYXRpb24xITAfBgNVBAsMGERlcGFydG1lbnQgb2YgSWRlbnRpdGllczEPMA0GA1UEAwwGbXkuaWRwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtZ6sClsZ55ulmtcZYIf18FPd5Hwc/TF4J32fu6WY0J0R9zHbFo+2HiSHSwM8jFHVCbKbvOS0bspHSh40RI/hfdWCnGujbZTZ0z9NPCluziruerKr3lb7BVTw3YGAA1h7TsdrsFlosTYFcn4dJcXwrLXkCQyi5wXC4IcPzNrL1Wuved97oxAsLdKopyBjvYRTaxFgNh5dfxyxX8RU+LIpcNnE8ZvKUy3QeN1idN1mywYCIoNnXXVu+hDpden4WIHcDOj/upYE2RqdywE1yV4KlxUx3Wgpc3382vEFtRXDHABi/qwnxiN3H7GCB0LK3eduasaBLJek0D10ONO4kWxQDSqVjzISWXVylSHijFX1IuIJoEHr3CRiY/e5xRIgVRnMfnUQu+yv6Fjp/aN5gMJypi2QHAXNWAS7bUjAvnHTfCMz2oPZokCtmQSqfNzq3RFHQye/dcLI8f4IwZ5Te96NV7egle3owXYdxrdCgkGglwdLOkf469Z6MjniH0doGtIF/mDlvr1Fww2Gp9E9gblaaLj5pVxpTKfVnw1OPcpDwfXiK39+Pz2m0mMKhhYBg8WZN8dddigfGmlZhBgbMPrqr7tBHm+c99MktskZq23fvVtsif7ARsG26Sm3lkCb03T4zOvPmENRcM2VAD7C2I+2IseVElCyEOadku3WZULYIP8CAwEAAaNQME4wHQYDVR0OBBYEFNHR4yCVyK5oB1LGcD+qXxeYEgOmMB8GA1UdIwQYMBaAFNHR4yCVyK5oB1LGcD+qXxeYEgOmMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAFrx1ggzUb+BKpvxWU9Nypb+jJyuRPYpBbvCnNohUcm8hdEE2wwybkesX01CKIH8KMjNBRytq/1aoR2nwog4aPXHciZBM5/zX2tIzDdfk4x3rRR6IdEfsXkC919TuVM+Uj/A+VgGHbqtGxEt1+nqTy9jRCOuALuYbFt3cycgqH2WjJazUPbVlkx2KdjkOmSYi1IrxrW3HXjuDPgpQKMFfaXiS7DwKzN4U5DIfTnX476u3N5oTNHfWK/JsqtrfQ061uJGEMt4P8BaliAqHoHt289XHD03wAm/b2ajgJQoW/Hhkpz8gN785doKHjEeoEkZzFllUj7e69SmD7HWDWAFeO1OOpikgwuX93f+YYccTnUAznqN8r05t5WjqPypizhr193Qc5TlEm+FkzlfjzivltTLCv0aJDlRlrQVOrZL4Buh0E4weZFk97jKeghLO6qnwxID/jSptEeN5L69Q8QzOFWia7SFGszlvrl8Y/3CQxYCzRmtSZmqdY9a6taZZ/w433+Ktc6vOstjqRE67yq1pp+0Ee4haMB/E87hDuh3Drcq2tT+b9wLNNfhSTYIw4AGpW27xhPh65RouOi9ufGjyFs1ZqqEEmBMIJi95KhUCFokMRf8L29ijqQqpDve8EvEYkBa7KgzTq3R925D5G55orLXoG4Bfdq9FY35hmR8TbLC
+
+
+
+
+
+
+
+ MIIF1TCCA72gAwIBAgIJAKib45YfneRFMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYDVQQGEwJVUzERMA8GA1UECAwITWlkbGFuZHMxEjAQBgNVBAcMCVNhZmV2aWxsZTEWMBQGA1UECgwNSUQgRmVkZXJhdGlvbjEhMB8GA1UECwwYRGVwYXJ0bWVudCBvZiBJZGVudGl0aWVzMQ8wDQYDVQQDDAZteS5pZHAwHhcNMTcwOTI5MjAyNDAxWhcNMTgwOTI5MjAyNDAxWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pZGxhbmRzMRIwEAYDVQQHDAlTYWZldmlsbGUxFjAUBgNVBAoMDUlEIEZlZGVyYXRpb24xITAfBgNVBAsMGERlcGFydG1lbnQgb2YgSWRlbnRpdGllczEPMA0GA1UEAwwGbXkuaWRwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtZ6sClsZ55ulmtcZYIf18FPd5Hwc/TF4J32fu6WY0J0R9zHbFo+2HiSHSwM8jFHVCbKbvOS0bspHSh40RI/hfdWCnGujbZTZ0z9NPCluziruerKr3lb7BVTw3YGAA1h7TsdrsFlosTYFcn4dJcXwrLXkCQyi5wXC4IcPzNrL1Wuved97oxAsLdKopyBjvYRTaxFgNh5dfxyxX8RU+LIpcNnE8ZvKUy3QeN1idN1mywYCIoNnXXVu+hDpden4WIHcDOj/upYE2RqdywE1yV4KlxUx3Wgpc3382vEFtRXDHABi/qwnxiN3H7GCB0LK3eduasaBLJek0D10ONO4kWxQDSqVjzISWXVylSHijFX1IuIJoEHr3CRiY/e5xRIgVRnMfnUQu+yv6Fjp/aN5gMJypi2QHAXNWAS7bUjAvnHTfCMz2oPZokCtmQSqfNzq3RFHQye/dcLI8f4IwZ5Te96NV7egle3owXYdxrdCgkGglwdLOkf469Z6MjniH0doGtIF/mDlvr1Fww2Gp9E9gblaaLj5pVxpTKfVnw1OPcpDwfXiK39+Pz2m0mMKhhYBg8WZN8dddigfGmlZhBgbMPrqr7tBHm+c99MktskZq23fvVtsif7ARsG26Sm3lkCb03T4zOvPmENRcM2VAD7C2I+2IseVElCyEOadku3WZULYIP8CAwEAAaNQME4wHQYDVR0OBBYEFNHR4yCVyK5oB1LGcD+qXxeYEgOmMB8GA1UdIwQYMBaAFNHR4yCVyK5oB1LGcD+qXxeYEgOmMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAFrx1ggzUb+BKpvxWU9Nypb+jJyuRPYpBbvCnNohUcm8hdEE2wwybkesX01CKIH8KMjNBRytq/1aoR2nwog4aPXHciZBM5/zX2tIzDdfk4x3rRR6IdEfsXkC919TuVM+Uj/A+VgGHbqtGxEt1+nqTy9jRCOuALuYbFt3cycgqH2WjJazUPbVlkx2KdjkOmSYi1IrxrW3HXjuDPgpQKMFfaXiS7DwKzN4U5DIfTnX476u3N5oTNHfWK/JsqtrfQ061uJGEMt4P8BaliAqHoHt289XHD03wAm/b2ajgJQoW/Hhkpz8gN785doKHjEeoEkZzFllUj7e69SmD7HWDWAFeO1OOpikgwuX93f+YYccTnUAznqN8r05t5WjqPypizhr193Qc5TlEm+FkzlfjzivltTLCv0aJDlRlrQVOrZL4Buh0E4weZFk97jKeghLO6qnwxID/jSptEeN5L69Q8QzOFWia7SFGszlvrl8Y/3CQxYCzRmtSZmqdY9a6taZZ/w433+Ktc6vOstjqRE67yq1pp+0Ee4haMB/E87hDuh3Drcq2tT+b9wLNNfhSTYIw4AGpW27xhPh65RouOi9ufGjyFs1ZqqEEmBMIJi95KhUCFokMRf8L29ijqQqpDve8EvEYkBa7KgzTq3R925D5G55orLXoG4Bfdq9FY35hmR8TbLC
+
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+
+
+ Jane
+ Doe
+ jane.doe@company.com
+
+
diff --git a/test/data/testshib_metadata.xml b/test/data/testshib_metadata.xml
new file mode 100644
index 0000000..bb5ed49
--- /dev/null
+++ b/test/data/testshib_metadata.xml
@@ -0,0 +1,302 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ testshib.org
+
+ TestShib Test IdP
+ TestShib IdP. Use this as a source of attributes
+ for your test SP.
+ https://www.testshib.org/testshibtwo.jpg
+
+
+
+
+
+
+
+
+
+ MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEB
+ CwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0
+ WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIB
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryh
+ m3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEm
+ lGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBn
+ xoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB
+ 3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTH
+ ot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQID
+ AQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQw
+ EoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzR
+ OZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QP
+ dRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS
+ 80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOT
+ MVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhO
+ RkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqX
+ MLRKhDgdmA==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ urn:mace:shibboleth:1.0:nameIdentifier
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEB
+ CwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0
+ WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIB
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryh
+ m3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEm
+ lGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBn
+ xoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB
+ 3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTH
+ ot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQID
+ AQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQw
+ EoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzR
+ OZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QP
+ dRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS
+ 80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOT
+ MVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhO
+ RkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqX
+ MLRKhDgdmA==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ urn:mace:shibboleth:1.0:nameIdentifier
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+
+
+ TestShib Two Identity Provider
+ TestShib Two
+ http://www.testshib.org/testshib-two/
+
+
+ Nate
+ Klingenstein
+ ndk@internet2.edu
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TestShib Test SP
+ TestShib SP. Log into this to test your machine.
+ Once logged in check that all attributes that you expected have been
+ released.
+ https://www.testshib.org/testshibtwo.jpg
+
+
+
+
+
+
+
+ MIIEPjCCAyagAwIBAgIBADANBgkqhkiG9w0BAQUFADB3MQswCQYDVQQGEwJVUzEV
+ MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMSIwIAYD
+ VQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQDEw9zcC50ZXN0
+ c2hpYi5vcmcwHhcNMDYwODMwMjEyNDM5WhcNMTYwODI3MjEyNDM5WjB3MQswCQYD
+ VQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1
+ cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQD
+ Ew9zcC50ZXN0c2hpYi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+ AQDJyR6ZP6MXkQ9z6RRziT0AuCabDd3x1m7nLO9ZRPbr0v1LsU+nnC363jO8nGEq
+ sqkgiZ/bSsO5lvjEt4ehff57ERio2Qk9cYw8XCgmYccVXKH9M+QVO1MQwErNobWb
+ AjiVkuhWcwLWQwTDBowfKXI87SA7KR7sFUymNx5z1aoRvk3GM++tiPY6u4shy8c7
+ vpWbVfisfTfvef/y+galxjPUQYHmegu7vCbjYP3On0V7/Ivzr+r2aPhp8egxt00Q
+ XpilNai12LBYV3Nv/lMsUzBeB7+CdXRVjZOHGuQ8mGqEbsj8MBXvcxIKbcpeK5Zi
+ JCVXPfarzuriM1G5y5QkKW+LAgMBAAGjgdQwgdEwHQYDVR0OBBYEFKB6wPDxwYrY
+ StNjU5P4b4AjBVQVMIGhBgNVHSMEgZkwgZaAFKB6wPDxwYrYStNjU5P4b4AjBVQV
+ oXukeTB3MQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYD
+ VQQHEwpQaXR0c2J1cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3Zp
+ ZGVyMRgwFgYDVQQDEw9zcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
+ BgkqhkiG9w0BAQUFAAOCAQEAc06Kgt7ZP6g2TIZgMbFxg6vKwvDL0+2dzF11Onpl
+ 5sbtkPaNIcj24lQ4vajCrrGKdzHXo9m54BzrdRJ7xDYtw0dbu37l1IZVmiZr12eE
+ Iay/5YMU+aWP1z70h867ZQ7/7Y4HW345rdiS6EW663oH732wSYNt9kr7/0Uer3KD
+ 9CuPuOidBacospDaFyfsaJruE99Kd6Eu/w5KLAGG+m0iqENCziDGzVA47TngKz2v
+ PVA+aokoOyoz3b53qeti77ijatSEoKjxheBWpO+eoJeGq/e49Um3M2ogIX/JAlMa
+ Inh+vYSYngQB2sx9LGkR9KHaMKNIGCDehk93Xla4pWJx1w==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+ urn:mace:shibboleth:1.0:nameIdentifier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TestShib Two Service Provider
+ TestShib Two
+ http://www.testshib.org/testshib-two/
+
+
+ Nate
+ Klingenstein
+ ndk@internet2.edu
+
+
+
+
+
+
+
diff --git a/test/samly_idp_data_test.exs b/test/samly_idp_data_test.exs
new file mode 100644
index 0000000..a410b2c
--- /dev/null
+++ b/test/samly_idp_data_test.exs
@@ -0,0 +1,161 @@
+defmodule SamlyIdpDataTest do
+ use ExUnit.Case
+ require Samly.Esaml
+ alias Samly.{Esaml, IdpData, SpData}
+
+ @sp_config1 %{
+ id: "sp1",
+ entity_id: "urn:test:sp1",
+ certfile: "test/data/test.crt",
+ keyfile: "test/data/test.pem"
+ }
+
+ @idp_config1 %{
+ id: "idp1",
+ sp_id: "sp1",
+ base_url: "http://samly.howto:4003/sso",
+ metadata_file: "test/data/idp_metadata.xml"
+ }
+
+ setup context do
+ sp_data = SpData.load_provider(@sp_config1)
+ [sps: %{sp_data.id => sp_data}] |> Enum.into(context)
+ end
+
+ test "valid-idp-config-1", %{sps: sps} do
+ %IdpData{} = idp_data = IdpData.load_provider(@idp_config1, sps)
+ assert idp_data.valid?
+ end
+
+ # verify defaults
+ test "valid-idp-config-2", %{sps: sps} do
+ %IdpData{} = idp_data = IdpData.load_provider(@idp_config1, sps)
+ refute idp_data.use_redirect_for_req
+ assert idp_data.sign_requests
+ assert idp_data.sign_metadata
+ assert idp_data.signed_assertion_in_resp
+ assert idp_data.signed_envelopes_in_resp
+ end
+
+ test "valid-idp-config-3", %{sps: sps} do
+ idp_config =
+ Map.merge(@idp_config1, %{
+ use_redirect_for_req: false,
+ sign_requests: true,
+ sign_metadata: true,
+ signed_assertion_in_resp: true,
+ signed_envelopes_in_resp: true
+ })
+
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ refute idp_data.use_redirect_for_req
+ assert idp_data.sign_requests
+ assert idp_data.sign_metadata
+ assert idp_data.signed_assertion_in_resp
+ assert idp_data.signed_envelopes_in_resp
+ end
+
+ test "valid-idp-config-4", %{sps: sps} do
+ idp_config =
+ Map.merge(@idp_config1, %{
+ use_redirect_for_req: true,
+ sign_requests: false,
+ sign_metadata: false,
+ signed_assertion_in_resp: false,
+ signed_envelopes_in_resp: false
+ })
+
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ assert idp_data.use_redirect_for_req
+ refute idp_data.sign_requests
+ refute idp_data.sign_metadata
+ refute idp_data.signed_assertion_in_resp
+ refute idp_data.signed_envelopes_in_resp
+ end
+
+ test "valid-idp-config-5", %{sps: sps} do
+ idp_config = %{@idp_config1 | base_url: nil}
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ assert idp_data.valid?
+ assert idp_data.base_url == nil
+ end
+
+ test "valid-idp-config-6", %{sps: sps} do
+ idp_config = Map.put(@idp_config1, :pre_session_create_pipeline, MyPipeline)
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ assert idp_data.valid?
+ assert idp_data.pre_session_create_pipeline == MyPipeline
+ end
+
+ test "valid-idp-config-7", %{sps: sps} do
+ idp_config = %{@idp_config1 | metadata_file: "test/data/azure_fed_metadata.xml"}
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ assert idp_data.valid?
+ end
+
+ test "valid-idp-config-8", %{sps: sps} do
+ idp_config = %{@idp_config1 | metadata_file: "test/data/onelogin_idp_metadata.xml"}
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ assert idp_data.valid?
+ end
+
+ test "valid-idp-config-9", %{sps: sps} do
+ idp_config = %{@idp_config1 | metadata_file: "test/data/shibboleth_idp_metadata.xml"}
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ assert idp_data.valid?
+ end
+
+ test "valid-idp-config-10", %{sps: sps} do
+ idp_config = %{@idp_config1 | metadata_file: "test/data/simplesaml_idp_metadata.xml"}
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ assert idp_data.valid?
+ end
+
+ test "valid-idp-config-11", %{sps: sps} do
+ idp_config = %{@idp_config1 | metadata_file: "test/data/testshib_metadata.xml"}
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ assert idp_data.valid?
+ end
+
+ test "url-test-1", %{sps: sps} do
+ idp_config = %{@idp_config1 | metadata_file: "test/data/shibboleth_idp_metadata.xml"}
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ assert idp_data.valid?
+
+ Esaml.esaml_idp_metadata(
+ login_location: sso_url,
+ logout_location: slo_url
+ ) = idp_data.esaml_idp_rec
+
+ assert sso_url |> List.to_string() |> String.ends_with?("/SAML2/POST/SSO")
+ assert slo_url |> List.to_string() |> String.ends_with?("/SAML2/POST/SLO")
+ end
+
+ test "url-test-2", %{sps: sps} do
+ idp_config = %{@idp_config1 | metadata_file: "test/data/shibboleth_idp_metadata.xml"}
+ idp_config = Map.put(idp_config, :use_redirect_for_req, true)
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ assert idp_data.valid?
+
+ Esaml.esaml_idp_metadata(
+ login_location: sso_url,
+ logout_location: slo_url
+ ) = idp_data.esaml_idp_rec
+
+ assert sso_url |> List.to_string() |> String.ends_with?("/SAML2/Redirect/SSO")
+ assert slo_url |> List.to_string() |> String.ends_with?("/SAML2/Redirect/SLO")
+ end
+
+ @tag :skip
+ test "invalid-idp-config-1", %{sps: sps} do
+ idp_config = %{@idp_config1 | id: ""}
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ refute idp_data.valid?
+ end
+
+ test "invalid-idp-config-2", %{sps: sps} do
+ idp_config = %{@idp_config1 | sp_id: "unknown-sp"}
+ %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
+ refute idp_data.valid?
+ end
+end
diff --git a/test/samly_sp_data_test.exs b/test/samly_sp_data_test.exs
new file mode 100644
index 0000000..6cf5e0b
--- /dev/null
+++ b/test/samly_sp_data_test.exs
@@ -0,0 +1,59 @@
+defmodule SamlySpDataTest do
+ use ExUnit.Case
+ alias Samly.SpData
+
+ @sp_config1 %{
+ id: "sp1",
+ entity_id: "urn:test:sp1",
+ certfile: "test/data/test.crt",
+ keyfile: "test/data/test.pem"
+ }
+
+ test "valid-sp-config-1" do
+ %SpData{} = sp_data = SpData.load_provider(@sp_config1)
+ assert sp_data.valid?
+ end
+
+ test "invalid-sp-config-1" do
+ sp_config = %{@sp_config1 | id: ""}
+ %SpData{} = sp_data = SpData.load_provider(sp_config)
+ refute sp_data.valid?
+ end
+
+ test "invalid-sp-config-2" do
+ sp_config = %{@sp_config1 | certfile: ""}
+ %SpData{} = sp_data = SpData.load_provider(sp_config)
+ refute sp_data.valid?
+ end
+
+ test "invalid-sp-config-3" do
+ sp_config = %{@sp_config1 | keyfile: ""}
+ %SpData{} = sp_data = SpData.load_provider(sp_config)
+ refute sp_data.valid?
+ end
+
+ test "invalid-sp-config-4" do
+ sp_config = %{@sp_config1 | certfile: "non-existent.crt"}
+ %SpData{} = sp_data = SpData.load_provider(sp_config)
+ refute sp_data.valid?
+ end
+
+ test "invalid-sp-config-5" do
+ sp_config = %{@sp_config1 | keyfile: "non-existent.pem"}
+ %SpData{} = sp_data = SpData.load_provider(sp_config)
+ refute sp_data.valid?
+ end
+
+ test "invalid-sp-config-6" do
+ sp_config = %{@sp_config1 | certfile: "test/data/test.pem"}
+ %SpData{} = sp_data = SpData.load_provider(sp_config)
+ refute sp_data.valid?
+ end
+
+ @tag :skip
+ test "invalid-sp-config-7" do
+ sp_config = %{@sp_config1 | keyfile: "test/data/test.crt"}
+ %SpData{} = sp_data = SpData.load_provider(sp_config)
+ refute sp_data.valid?
+ end
+end
diff --git a/test/samly_test.exs b/test/samly_test.exs
deleted file mode 100644
index 9836ef8..0000000
--- a/test/samly_test.exs
+++ /dev/null
@@ -1,40 +0,0 @@
-defmodule SamlyTest do
- use ExUnit.Case
- doctest Samly
-
- @test_opts [
- certfile: "test/data/test.crt",
- keyfile: "test/data/test.pem",
- idp_metadata_file: "test/data/idp_metadata.xml",
- base_url: "http://my.app:4000/sso"
- ]
-
- test "valid sp and idp config" do
- assert Samly.Provider.load_sp_idp_rec(@test_opts)
- end
-
- test "missing sp certfile" do
- opts = Keyword.drop(@test_opts, [:certfile])
- assert :error = Samly.Provider.load_sp_idp_rec(opts)
- end
-
- test "missing sp keyfile" do
- opts = Keyword.drop(@test_opts, [:keyfile])
- assert :error = Samly.Provider.load_sp_idp_rec(opts)
- end
-
- test "missing idp metadata" do
- opts = Keyword.drop(@test_opts, [:idp_metadata_file])
- assert :error = Samly.Provider.load_sp_idp_rec(opts)
- end
-
- test "invalid certfile" do
- opts = Keyword.put(@test_opts, :certfile, @test_opts[:keyfile])
- assert :error = Samly.Provider.load_sp_idp_rec(opts)
- end
-
- test "invalid keyfile" do
- opts = Keyword.put(@test_opts, :keyfile, @test_opts[:idp_metadata_file])
- assert :error = Samly.Provider.load_sp_idp_rec(opts)
- end
-end