-module(hund_xml). -include("../include/hund.hrl"). -include("../include/hund_xpath_macro.hrl"). -include_lib("xmerl/include/xmerl.hrl"). -type xml_thing() :: #xmlDocument{}. -export( [ to_xml/1, sign_xml/3, build_nsinfo/2, decode_sp_metadata/1, decode_authn_request/1, decode_logout_request/1, decode_logout_response/1 ] ). -spec to_xml(Data :: hund:saml_record()) -> #xmlElement{}. to_xml( #saml_idp_metadata{ entity_id = EntityId, certificate = Certificate, login_location = LoginLocation, logout_location = LogoutLocation, org = #saml_org{name = OrgName, display_name = OrgDisplayName, url = OrgUrl}, tech = #saml_contact{name = TechName, email = TechEmail}, signed_request = SignedRequest, name_format = NameFormat } ) -> Ns = #xmlNamespace{ nodes = [ {"md", 'urn:oasis:names:tc:SAML:2.0:metadata'}, {"ds", 'http://www.w3.org/2000/09/xmldsig#'} ] }, KeyDescriptor = case is_binary(Certificate) of false -> []; true -> [ #xmlElement{ name = 'md:KeyDescriptor', attributes = [#xmlAttribute{name = use, value = "signing"}], content = [ #xmlElement{ name = 'ds:KeyInfo', content = [ #xmlElement{ name = 'ds:X509Data', content = [ #xmlElement{ name = 'ds:X509Certificate', content = [#xmlText{value = base64:encode_to_string(Certificate)}] } ] } ] } ] }, #xmlElement{ name = 'md:KeyDescriptor', attributes = [#xmlAttribute{name = use, value = "encryption"}], content = [ #xmlElement{ name = 'ds:KeyInfo', content = [ #xmlElement{ name = 'ds:X509Data', content = [ #xmlElement{ name = 'ds:X509Certificate', content = [#xmlText{value = base64:encode_to_string(Certificate)}] } ] } ] } ] } ] end, SSoElement = case LoginLocation of "" -> []; _ -> [ #xmlElement{ name = 'md:SingleSignOnService', attributes = [ #xmlAttribute{ name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, #xmlAttribute{name = 'Location', value = LoginLocation}, #xmlAttribute{name = index, value = "0"}, #xmlAttribute{name = isDefault, value = "true"} ] }, #xmlElement{ name = 'md:SingleSignOnService', attributes = [ #xmlAttribute{ name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, #xmlAttribute{name = 'Location', value = LoginLocation}, #xmlAttribute{name = index, value = "1"} ] } ] end, SLoElement = case LogoutLocation of undefined -> []; _ -> [ #xmlElement{ name = 'md:SingleLogoutService', attributes = [ #xmlAttribute{ name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, #xmlAttribute{name = 'Location', value = LogoutLocation}, #xmlAttribute{name = index, value = "0"}, #xmlAttribute{name = isDefault, value = "true"} ] }, #xmlElement{ name = 'md:SingleLogoutService', attributes = [ #xmlAttribute{ name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, #xmlAttribute{name = 'Location', value = LogoutLocation}, #xmlAttribute{name = index, value = "1"} ] } ] end, OrgLocation = [ #xmlElement{ name = 'md:Organization', content = lang_elems(#xmlElement{name = 'md:OrganizationName'}, OrgName) ++ lang_elems(#xmlElement{name = 'md:OrganizationDisplayName'}, OrgDisplayName) ++ lang_elems(#xmlElement{name = 'md:OrganizationURL'}, OrgUrl) } ], TechElement = [ #xmlElement{ name = 'md:ContactPerson', content = [ #xmlElement{name = 'md:GivenName', content = [#xmlText{value = TechName}]}, #xmlElement{name = 'md:EmailAddress', content = [#xmlText{value = TechEmail}]} ], attributes = [#xmlAttribute{name = contactType, value = "technical"}] } ], NameIdElement = [ #xmlElement{ name = 'md:NameIDFormat', content = [#xmlText{value = hund:rev_nameid_map(NameFormat)}] } ], IDPSSODescriptor = [ #xmlElement{ name = 'md:IDPSSODescriptor', content = KeyDescriptor ++ SSoElement ++ SLoElement ++ NameIdElement, attributes = [#xmlAttribute{name = 'WantAuthnRequestsSigned', value = atom_to_list(SignedRequest)}] } ], build_nsinfo( Ns, #xmlElement{ name = 'md:EntityDescriptor', content = IDPSSODescriptor ++ OrgLocation ++ TechElement, attributes = [ (#xmlAttribute{name = 'Version', value = "2.0"}) #xmlAttribute{name = entityID, value = EntityId}, #xmlAttribute{name = 'xmlns:md', value = proplists:get_value(md, Ns#xmlNamespace.nodes)}, #xmlAttribute{name = 'xmlns:ds', value = proplists:get_value(ds, Ns#xmlNamespace.nodes)} ] } ); to_xml( #saml_assertion{ issuer = Issuer, status = Status, recipient = Recipient, conditions = #saml_condition{ not_before = ConditionNotBefore, not_on_or_after = ConditionNotOnOrAfter, audience = Audience }, attributes = Attributes, authn = #saml_authn{ authn_instant = AuthnInstant, session_not_on_or_after = SessionNotOnOrAfter, session_index = SessionIndex, authn_class = AuthnClass }, subject = #saml_subject{ in_response_to = InResponseTo, confirmation_method = ConfirmationMethod, name = Name, name_format = NameFormat, not_on_or_after = Notonorafter, sp_name_qualifier = SpNameQualifier } } ) -> Ns = #xmlNamespace{ nodes = [ {"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'}, {"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}, {"xsi", 'http://www.w3.org/2001/XMLSchema-instance'}, {"xs", 'http://www.w3.org/2001/XMLSchema'} ] }, IssuerElement = [#xmlElement{name = 'saml:Issuer', content = [#xmlText{value = Issuer}]}], StatusElement = [ #xmlElement{ name = 'samlp:Status', content = [ #xmlElement{ name = 'samlp:StatusCode', attributes = [#xmlAttribute{name = 'Value', value = hund:rev_status_code_map(Status)}] } ] } ], AssertionSubjectElement = [ #xmlElement{ name = 'saml:Subject', content = [ #xmlElement{ name = 'saml:NameID', content = [#xmlText{value = Name}], attributes = [ #xmlAttribute{name = 'SPNameQualifier', value = SpNameQualifier}, #xmlAttribute{name = 'Format', value = hund:rev_nameid_map(NameFormat)} ] }, #xmlElement{ name = 'saml:SubjectConfirmation', attributes = [ #xmlAttribute{ name = 'Method', value = hund:rev_subject_method_map(ConfirmationMethod) } ], content = [ #xmlElement{ name = 'saml:SubjectConfirmationData', attributes = [ #xmlAttribute{ name = 'NotOnOrAfter', value = hund:datetime_to_saml(Notonorafter) }, #xmlAttribute{name = 'Recipient', value = Recipient}, #xmlAttribute{name = 'InResponseTo', value = InResponseTo} ] } ] } ] } ], ConditionElement = [ #xmlElement{ name = 'saml:Conditions', attributes = [ #xmlAttribute{name = 'NotBefore', value = hund:datetime_to_saml(ConditionNotBefore)}, #xmlAttribute{ name = 'NotOnOrAfter', value = hund:datetime_to_saml(ConditionNotOnOrAfter) } ], content = [ #xmlElement{ name = 'saml:AudienceRestriction', content = [ #xmlElement{ name = 'saml:Audience', content = [#xmlText{value = stringify(Audience)}] } ] } ] } ], AuthnStatement = [ #xmlElement{ name = 'saml:AuthnStatement', attributes = [ #xmlAttribute{name = 'AuthnInstant', value = hund:datetime_to_saml(AuthnInstant)}, #xmlAttribute{name = 'SessionIndex', value = SessionIndex}, #xmlAttribute{ name = 'SessionNotOnOrAfter', value = hund:datetime_to_saml(SessionNotOnOrAfter) } ], content = [ #xmlElement{ name = 'saml:AuthnContext', content = [ #xmlElement{ name = 'saml:AuthnContextClassRef', content = [#xmlText{value = hund:rev_map_authn_class(AuthnClass)}] } ] } ] } ], AttributeStatement = [ #xmlElement{ name = 'saml:AttributeStatement', content = [ #xmlElement{ name = 'saml:Attribute', attributes = [ #xmlAttribute{name = 'Name', value = Key}, #xmlAttribute{ name = 'NameFormat', value = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic" } ], content = [ #xmlElement{ name = 'saml:AttributeValue', attributes = [#xmlAttribute{name = 'xsi:type', value = xml_type_schema(Value)}], content = [#xmlText{value = stringify(Value)}] } ] } || {Key, Value} <- Attributes ] } ], Assertion = [ #xmlElement{ name = 'saml:Assertion', content = IssuerElement ++ AssertionSubjectElement ++ ConditionElement ++ AuthnStatement ++ AttributeStatement, attributes = [ #xmlAttribute{name = 'Version', value = '2.0'}, #xmlAttribute{ name = 'IssueInstant', value = hund:datetime_to_saml(calendar:universal_time()) } ] } ], build_nsinfo( Ns, #xmlElement{ name = 'samlp:Response', content = IssuerElement ++ StatusElement ++ Assertion, attributes = [ #xmlAttribute{name = 'Version', value = "2.0"}, #xmlAttribute{name = 'xmlns:samlp', value = "urn:oasis:names:tc:SAML:2.0:protocol"}, #xmlAttribute{name = 'xmlns:saml', value = "urn:oasis:names:tc:SAML:2.0:assertion"}, #xmlAttribute{name = 'xmlns:xsi', value = "http://www.w3.org/2001/XMLSchema-instance"}, #xmlAttribute{name = 'xmlns:xs', value = "http://www.w3.org/2001/XMLSchema"} ] } ); to_xml( #saml_logout_response{ destination = Destination, in_response_to = InResponseTo, issuer = Issuer, status = Status, issue_instant = IssueInstant } ) -> Ns = #xmlNamespace{ nodes = [ {"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'}, {"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'} ] }, esaml_util:build_nsinfo( Ns, #xmlElement{ name = 'samlp:LogoutResponse', attributes = [ #xmlAttribute{name = 'xmlns:samlp', value = "urn:oasis:names:tc:SAML:2.0:protocol"}, #xmlAttribute{name = 'xmlns:saml', value = "urn:oasis:names:tc:SAML:2.0:assertion"}, #xmlAttribute{name = 'Destination', value = Destination}, #xmlAttribute{name = 'InResponseTo', value = InResponseTo}, #xmlAttribute{name = 'IssueInstant', value = hund:datetime_to_saml(IssueInstant)}, #xmlAttribute{name = 'Version', value = "2.0"} ], content = [ #xmlElement{name = 'saml:Issuer', content = [#xmlText{value = Issuer}]}, #xmlElement{ name = 'samlp:Status', content = [ #xmlElement{ name = 'samlp:StatusCode', attributes = [#xmlAttribute{name = 'Value', value = hund:rev_status_code_map(Status)}] } ] } ] } ); to_xml( #saml_logout_request{ issue_instant = IssueInstant, issuer = Issuer, sp_name_qualifier = SpNameQualifier, name_format = NameFormat, name = Name, destination = Destination } ) -> Ns = #xmlNamespace{ nodes = [ {"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'}, {"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'} ] }, build_nsinfo( Ns, #xmlElement{ name = 'samlp:LogoutRequest', attributes = [ #xmlAttribute{name = 'xmlns:samlp', value = "urn:oasis:names:tc:SAML:2.0:protocol"}, #xmlAttribute{name = 'xmlns:saml', value = "urn:oasis:names:tc:SAML:2.0:assertion"}, #xmlAttribute{name = 'Destination', value = Destination}, #xmlAttribute{name = 'IssueInstant', value = hund:datetime_to_saml(IssueInstant)}, #xmlAttribute{name = 'Version', value = "2.0"} ], content = [ #xmlElement{name = 'saml:Issuer', content = [#xmlText{value = Issuer}]}, #xmlElement{ name = 'saml:NameID', attributes = [ #xmlAttribute{name = 'SPNameQualifier', value = SpNameQualifier}, #xmlAttribute{name = 'Format', value = hund:rev_nameid_map(NameFormat)} ], content = [#xmlText{value = Name}] } ] } ). -spec sign_xml(Xml :: #xmlDocument{}, Path :: string(), fun((xml_thing()) -> xml_thing())) -> {ok, #xmlDocument{}} | {error, term()}. sign_xml(Xml, Path, F) -> Ns = #xmlNamespace{ nodes = [ {"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'}, {"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}, {"xsi", 'http://www.w3.org/2001/XMLSchema-instance'}, {"xs", 'http://www.w3.org/2001/XMLSchema'} ] }, case string:tokens(Path, "/") of [] -> F(Xml); [_Root] -> F(Xml); Paths -> Last = lists:last(Paths), Rest = lists:droplast(Paths), NewPath = lists:flatten("/" ++ lists:join("/", Rest) ++ "/*[not(self::" ++ Last ++ ")]"), OtherEl = case xmerl_xpath:string(NewPath, Xml, [{namespace, Ns}]) of undefined -> []; [] -> []; Result -> Result end, case xmerl_xpath:string(Path, Xml, [{namespace, Ns}]) of [Element] -> Res = F(Element), {ok, Xml#xmlElement{content = [Res] ++ OtherEl}}; _ -> {error, invalid_xpath} end end. -spec decode_sp_metadata(Doc :: string() | #xmlElement{}) -> {ok, #saml_sp_metadata{}} | {error, term()}. decode_sp_metadata(Doc) when is_list(Doc) -> {Xml, _Rest} = xmerl_scan:string(Doc, [{namespace_conformant, true}]), decode_sp_metadata(Xml); decode_sp_metadata(Xml = #xmlElement{}) -> Ns = [{md, "urn:oasis:names:tc:SAML:2.0:metadata"}, {ds, "http://www.w3.org/2000/09/xmldsig#"}], hund:threaduntil( [ ?xpath_attr_required( "/md:EntityDescriptor/@entityID", saml_sp_metadata, entity_id, bad_entity ), ?xpath_attr_required( "/md:EntityDescriptor/md:SPSSODescriptor/md:AssertionConsumerService[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']/@Location", saml_sp_metadata, consumer_location, missing_consumer_location ), ?xpath_attr( "/md:EntityDescriptor/md:SPSSODescriptor/@AuthnRequestsSigned", saml_sp_metadata, signed_request, fun list_to_atom/1 ), ?xpath_attr( "/md:EntityDescriptor/md:SPSSODescriptor/@WantAssertionsSigned", saml_sp_metadata, signed_assertion, fun list_to_atom/1 ), ?xpath_attr( "/md:EntityDescriptor/md:SPSSODescriptor/md:SingleLogoutService[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']/@Location", saml_sp_metadata, logout_location ), ?xpath_text( "/md:EntityDescriptor/md:SPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()", saml_sp_metadata, certificate, fun base64:decode_to_string/1 ), ?xpath_recurse( "/md:EntityDescriptor/md:Organization", saml_sp_metadata, org, fun decode_org/1 ), ?xpath_recurse( "/md:EntityDescriptor/md:ContactPerson[@contactType='technical']", saml_sp_metadata, tech, fun decode_contact_person/1 ) ], #saml_sp_metadata{} ). -spec decode_authn_request(Doc :: string() | #xmlElement{}) -> {ok, #saml_authnreq{}} | {error, bad_issuer} | {error, missing_consumer_location} | {error, term()}. decode_authn_request(Doc) when is_list(Doc) -> {Xml, _Rest} = xmerl_scan:string(Doc, [{namespace_conformant, true}]), decode_authn_request(Xml); decode_authn_request(Xml = #xmlElement{}) -> Ns = [ {samlp, "urn:oasis:names:tc:SAML:2.0:protocol"}, {saml, "urn:oasis:names:tc:SAML:2.0:assertion"}, {ds, "http://www.w3.org/2000/09/xmldsig#"} ], hund:threaduntil( [ ?xpath_text_required( "/samlp:AuthnRequest/saml:Issuer/text()", saml_authnreq, issuer, bad_issuer ), ?xpath_attr_required( "/samlp:AuthnRequest/@AssertionConsumerServiceURL", saml_authnreq, consumer_location, missing_consumer_location ), ?xpath_text( "/samlp:AuthnRequest/samlp:RequestedAuthnContext/saml:AuthnContextClassRef/text()", saml_authnreq, authn_class, fun hund:map_authn_class/1 ), ?xpath_attr( "/samlp:AuthnRequest/samlp:NameIDPolicy/@Format", saml_authnreq, name_format, fun hund:nameid_map/1 ), ?xpath_attr("/samlp:AuthnRequest/@Version", saml_authnreq, version), ?xpath_attr( "/samlp:AuthnRequest/@IssueInstant", saml_authnreq, issue_instant, fun hund:saml_to_datetime/1 ), ?xpath_attr("/samlp:AuthnRequest/@Destination", saml_authnreq, destination) ], #saml_authnreq{} ). -spec decode_logout_request(Doc :: string() | #xmlElement{}) -> {ok, #saml_logout_request{}} | {error, bad_issuer} | {error, term()}. decode_logout_request(Doc) when is_list(Doc) -> {Xml, _Rest} = xmerl_scan:string(Doc, [{namespace_conformant, true}]), decode_logout_request(Xml); decode_logout_request(Xml = #xmlElement{}) -> Ns = [ {samlp, "urn:oasis:names:tc:SAML:2.0:protocol"}, {saml, "urn:oasis:names:tc:SAML:2.0:assertion"}, {ds, "http://www.w3.org/2000/09/xmldsig#"} ], hund:threaduntil( [ ?xpath_text_required( "/samlp:LogoutRequest/saml:Issuer/text()", saml_logout_request, issuer, bad_issuer ), ?xpath_attr( "/samlp:LogoutRequest/saml:NameID/@Format", saml_logout_request, name_format, fun hund:nameid_map/1 ), ?xpath_text("/samlp:LogoutRequest/saml:NameID/text()", saml_logout_request, name), ?xpath_attr( "/samlp:LogoutRequest/saml:NameID/@SPNameQualifier", saml_logout_request, sp_name_qualifier ), ?xpath_attr( "/samlp:LogoutRequest/@IssueInstant", saml_logout_request, issue_instant, fun hund:saml_to_datetime/1 ), ?xpath_attr("/samlp:LogoutResponse/@Destination", saml_logout_response, destination), ?xpath_text( "/samlp:LogoutRequest/samlp:SessionIndex/text()", saml_logout_request, session_index ) ], #saml_logout_request{} ). -spec decode_logout_response(Doc :: string() | #xmlElement{}) -> {ok, #saml_logout_request{}} | {error, bad_issuer} | {error, term()}. decode_logout_response(Doc) when is_list(Doc) -> {Xml, _Rest} = xmerl_scan:string(Doc, [{namespace_conformant, true}]), decode_logout_response(Xml); decode_logout_response(Xml = #xmlElement{}) -> Ns = [ {samlp, "urn:oasis:names:tc:SAML:2.0:protocol"}, {saml, "urn:oasis:names:tc:SAML:2.0:assertion"}, {ds, "http://www.w3.org/2000/09/xmldsig#"} ], hund:threaduntil( [ ?xpath_text_required( "/samlp:LogoutResponse/saml:Issuer/text()", saml_logout_response, issuer, bad_issuer ), ?xpath_attr( "/samlp:LogoutResponse/@IssueInstant", saml_logout_response, issue_instant, fun hund:saml_to_datetime/1 ), ?xpath_attr("/samlp:LogoutResponse/@Destination", saml_logout_response, destination), ?xpath_attr("/samlp:LogoutResponse/@InResponseTo", saml_logout_response, in_response_to), ?xpath_attr( "/samlp:LogoutResponse/samlp:Status/samlp:StatusCode/@Value", saml_logout_response, status, fun hund:status_code_map/1 ) ], #saml_logout_response{} ). -spec build_nsinfo(#xmlNamespace{}, #xmlElement{}) -> #xmlElement{}. build_nsinfo(Ns, Attr = #xmlAttribute{name = Name}) -> case string:tokens(atom_to_list(Name), ":") of [NsPrefix, Rest] -> Attr#xmlAttribute{namespace = Ns, nsinfo = {NsPrefix, Rest}}; _ -> Attr#xmlAttribute{namespace = Ns} end; build_nsinfo(Ns, Elem = #xmlElement{name = Name, content = Children, attributes = Attrs}) -> Elem2 = case string:tokens(atom_to_list(Name), ":") of [NsPrefix, Rest] -> Elem#xmlElement{namespace = Ns, nsinfo = {NsPrefix, Rest}}; _ -> Elem#xmlElement{namespace = Ns} end, Elem2#xmlElement{ attributes = [build_nsinfo(Ns, Attr) || Attr <- Attrs], content = [build_nsinfo(Ns, Child) || Child <- Children] }; build_nsinfo(_Ns, Other) -> Other. -spec lang_elems(#xmlElement{}, hund:localized_string()) -> [#xmlElement{}]. lang_elems(BaseTag, Vals = [{Lang, _} | _]) when is_atom(Lang) -> [ BaseTag#xmlElement{ attributes = BaseTag#xmlElement.attributes ++ [#xmlAttribute{name = 'xml:lang', value = atom_to_list(L)}], content = BaseTag#xmlElement.content ++ [#xmlText{value = V}] } || {L, V} <- Vals ]; lang_elems(BaseTag, Val) -> [ BaseTag#xmlElement{ attributes = BaseTag#xmlElement.attributes ++ [#xmlAttribute{name = 'xml:lang', value = "en"}], content = BaseTag#xmlElement.content ++ [#xmlText{value = Val}] } ]. -spec xml_type_schema(Scalar :: term()) -> atom(). xml_type_schema(Scalar) when is_integer(Scalar) -> 'xs:integer'; xml_type_schema(Scalar) when is_boolean(Scalar) -> 'xs:boolean'; xml_type_schema(Scalar) when is_float(Scalar) -> 'xs:float'; xml_type_schema({{Year, Month, Day}}) when is_integer(Year) andalso is_integer(Month) andalso is_integer(Day) -> 'xs:date'; xml_type_schema({{Year, Month, Day}, {Hour, Minute, Second}}) when is_integer(Year) andalso is_integer(Month) and is_integer(Day) andalso is_integer(Hour) andalso is_integer(Minute) andalso is_integer(Second) -> 'xs:dateTime'; xml_type_schema(_) -> 'xs:string'. -spec stringify(Scalar :: term()) -> string() | number(). stringify(Date = {Year, Month, Day}) when is_integer(Year) andalso is_integer(Month) andalso is_integer(Day) -> hund:date_to_saml(Date); stringify(DateTime = {{Year, Month, Day}, {Hour, Minute, Second}}) when is_integer(Year) andalso is_integer(Month) and is_integer(Day) andalso is_integer(Hour) andalso is_integer(Minute) andalso is_integer(Second) -> hund:datetime_to_saml(DateTime); stringify(Scalar) when is_atom(Scalar) -> atom_to_binary(Scalar); stringify(Scalar) -> Scalar. % private -spec decode_org(Xml :: #xmlElement{}) -> #saml_org{}. decode_org(Xml = #xmlElement{}) -> Ns = [{md, "urn:oasis:names:tc:SAML:2.0:metadata"}, {ds, "http://www.w3.org/2000/09/xmldsig#"}], hund:threaduntil( [ ?xpath_text("/md:Organization/md:OrganizationName/text()", saml_org, name), ?xpath_text("/md:Organization/md:OrganizationDisplayName/text()", saml_org, display_name), ?xpath_text("/md:Organization/md:OrganizationURL/text()", saml_org, url) ], #saml_org{} ). % private -spec decode_contact_person(Xml :: #xmlElement{}) -> #saml_contact{}. decode_contact_person(Xml = #xmlElement{}) -> Ns = [{md, "urn:oasis:names:tc:SAML:2.0:metadata"}, {ds, "http://www.w3.org/2000/09/xmldsig#"}], hund:threaduntil( [ ?xpath_text("/md:ContactPerson/md:GivenName/text()", saml_contact, name), ?xpath_text("/md:ContactPerson/md:EmailAddress/text()", saml_contact, email) ], #saml_contact{} ).