Current section
Files
Jump to
Current section
Files
src/esaml.erl
%% esaml - SAML for erlang
%%
%% Copyright (c) 2013, Alex Wilson and the University of Queensland
%% All rights reserved.
%%
%% Distributed subject to the terms of the 2-clause BSD license, see
%% the LICENSE file in the root of the distribution.
%% @doc SAML for Erlang
-module(esaml).
-behaviour(application).
-behaviour(supervisor).
-include_lib("xmerl/include/xmerl.hrl").
-include_lib("public_key/include/public_key.hrl").
-include("esaml.hrl").
-export([start/2, stop/1, init/1]).
-export([stale_time/1]).
-export([config/2, config/1, to_xml/1, decode_response/1, decode_assertion/1, validate_assertion/3]).
-export([decode_logout_request/1, decode_logout_response/1, decode_idp_metadata/1]).
-type org() :: #esaml_org{}.
-type contact() :: #esaml_contact{}.
-type sp_metadata() :: #esaml_sp_metadata{}.
-type idp_metadata() :: #esaml_idp_metadata{}.
-type authnreq() :: #esaml_authnreq{}.
-type subject() :: #esaml_subject{}.
-type assertion() :: #esaml_assertion{}.
-type logoutreq() :: #esaml_logoutreq{}.
-type logoutresp() :: #esaml_logoutresp{}.
-type response() :: #esaml_response{}.
-type sp() :: #esaml_sp{}.
-type saml_record() :: org() | contact() | sp_metadata() | idp_metadata() | authnreq() | subject() | assertion() | logoutreq() | logoutresp() | response().
-export_type([org/0, contact/0, sp_metadata/0, idp_metadata/0,
authnreq/0, subject/0, assertion/0, logoutreq/0,
logoutresp/0, response/0, sp/0, saml_record/0]).
-type localized_string() :: string() | [{Locale :: atom(), LocalizedString :: string()}].
-type name_format() :: email | x509 | windows | krb | persistent | transient | unknown.
-type logout_reason() :: user | admin.
-type status_code() :: success | request_error | response_error | bad_version | authn_failed | bad_attr | denied | bad_binding | unknown.
-type version() :: string().
-type datetime() :: string() | binary().
-type condition() :: {not_before, esaml:datetime()} | {not_on_or_after, esaml:datetime()} | {audience, string()}.
-type conditions() :: [condition()].
-export_type([localized_string/0, name_format/0, logout_reason/0, status_code/0, version/0, datetime/0, conditions/0]).
%% @private
start(_StartType, _StartArgs) ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% @private
stop(_State) ->
ok.
%% @private
init([]) ->
DupeEts = {esaml_ets_table_owner,
{esaml_util, start_ets, []},
permanent, 5000, worker, [esaml]},
{ok,
{{one_for_one, 60, 600},
[DupeEts]}}.
%% @doc Retrieve a config record
-spec config(Name :: atom()) -> term() | undefined.
config(N) -> config(N, undefined).
%% @doc Retrieve a config record with default
-spec config(Name :: atom(), Default :: term()) -> term().
config(N, D) ->
case application:get_env(esaml, N) of
{ok, V} -> V;
_ -> D
end.
-spec nameid_map(string()) -> name_format().
nameid_map("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress") -> email;
nameid_map("urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName") -> x509;
nameid_map("urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName") -> windows;
nameid_map("urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos") -> krb;
nameid_map("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent") -> persistent;
nameid_map("urn:oasis:names:tc:SAML:2.0:nameid-format:transient") -> transient;
nameid_map(S) when is_list(S) -> unknown.
-spec nameid_name_qualifier_map(string()) -> undefined | string().
nameid_name_qualifier_map("") -> undefined;
nameid_name_qualifier_map(S) when is_list(S) -> S.
-spec nameid_sp_name_qualifier_map(string()) -> undefined | string().
nameid_sp_name_qualifier_map("") -> undefined;
nameid_sp_name_qualifier_map(S) when is_list(S) -> S.
-spec nameid_format_map(string()) -> undefined | string().
nameid_format_map("") -> undefined;
nameid_format_map(S) when is_list(S) -> S.
-spec subject_method_map(string()) -> bearer | unknown.
subject_method_map("urn:oasis:names:tc:SAML:2.0:cm:bearer") -> bearer;
subject_method_map(_) -> unknown.
-spec status_code_map(string()) -> status_code() | atom().
status_code_map("urn:oasis:names:tc:SAML:2.0:status:Success") -> success;
status_code_map("urn:oasis:names:tc:SAML:2.0:status:VersionMismatch") -> bad_version;
status_code_map("urn:oasis:names:tc:SAML:2.0:status:AuthnFailed") -> authn_failed;
status_code_map("urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue") -> bad_attr;
status_code_map("urn:oasis:names:tc:SAML:2.0:status:RequestDenied") -> denied;
status_code_map("urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding") -> bad_binding;
status_code_map(Urn = "urn:" ++ _) -> list_to_atom(lists:last(string:tokens(Urn, ":")));
status_code_map(S) when is_list(S) -> unknown.
-spec rev_status_code_map(status_code()) -> string().
rev_status_code_map(success) -> "urn:oasis:names:tc:SAML:2.0:status:Success";
rev_status_code_map(bad_version) -> "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch";
rev_status_code_map(authn_failed) -> "urn:oasis:names:tc:SAML:2.0:status:AuthnFailed";
rev_status_code_map(bad_attr) -> "urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue";
rev_status_code_map(denied) -> "urn:oasis:names:tc:SAML:2.0:status:RequestDenied";
rev_status_code_map(bad_binding) -> "urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding";
rev_status_code_map(_) -> error(bad_status_code).
-spec logout_reason_map(string()) -> logout_reason().
logout_reason_map("urn:oasis:names:tc:SAML:2.0:logout:user") -> user;
logout_reason_map("urn:oasis:names:tc:SAML:2.0:logout:admin") -> admin;
logout_reason_map(S) when is_list(S) -> unknown.
-spec rev_logout_reason_map(logout_reason()) -> string().
rev_logout_reason_map(user) -> "urn:oasis:names:tc:SAML:2.0:logout:user";
rev_logout_reason_map(admin) -> "urn:oasis:names:tc:SAML:2.0:logout:admin".
-spec common_attrib_map(string()) -> atom().
common_attrib_map("urn:oid:2.16.840.1.113730.3.1.3") -> employeeNumber;
common_attrib_map("urn:oid:1.3.6.1.4.1.5923.1.1.1.6") -> eduPersonPrincipalName;
common_attrib_map("urn:oid:0.9.2342.19200300.100.1.3") -> mail;
common_attrib_map("urn:oid:2.5.4.42") -> givenName;
common_attrib_map("urn:oid:2.16.840.1.113730.3.1.241") -> displayName;
common_attrib_map("urn:oid:2.5.4.3") -> commonName;
common_attrib_map("urn:oid:2.5.4.20") -> telephoneNumber;
common_attrib_map("urn:oid:2.5.4.10") -> organizationName;
common_attrib_map("urn:oid:2.5.4.11") -> organizationalUnitName;
common_attrib_map("urn:oid:1.3.6.1.4.1.5923.1.1.1.9") -> eduPersonScopedAffiliation;
common_attrib_map("urn:oid:2.16.840.1.113730.3.1.4") -> employeeType;
common_attrib_map("urn:oid:0.9.2342.19200300.100.1.1") -> uid;
common_attrib_map("urn:oid:2.5.4.4") -> surName;
common_attrib_map(Uri = "http://" ++ _) -> list_to_atom(lists:last(string:tokens(Uri, "/")));
common_attrib_map(Other) when is_list(Other) -> list_to_atom(Other).
-include("xmerl_xpath_macros.hrl").
%% @private
-spec decode_idp_metadata(Xml :: #xmlElement{}) -> {ok, #esaml_idp_metadata{}} | {error, term()}.
decode_idp_metadata(Xml) ->
Ns = [{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'},
{"md", 'urn:oasis:names:tc:SAML:2.0:metadata'},
{"ds", 'http://www.w3.org/2000/09/xmldsig#'}],
esaml_util:threaduntil([
?xpath_attr_required("/md:EntityDescriptor/@entityID", esaml_idp_metadata, entity_id, bad_entity),
?xpath_attr_required("/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']/@Location",
esaml_idp_metadata, login_location, missing_sso_location),
?xpath_attr("/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']/@Location",
esaml_idp_metadata, logout_location),
?xpath_text("/md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat/text()",
esaml_idp_metadata, name_format, fun nameid_map/1),
?xpath_text("/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()", esaml_idp_metadata, certificate, fun(X) -> base64:decode(list_to_binary(X)) end),
?xpath_recurse("/md:EntityDescriptor/md:ContactPerson[@contactType='technical']", esaml_idp_metadata, tech, decode_contact),
?xpath_recurse("/md:EntityDescriptor/md:Organization", esaml_idp_metadata, org, decode_org)
], #esaml_idp_metadata{}).
%% @private
-spec decode_org(Xml :: #xmlElement{}) -> {ok, #esaml_org{}} | {error, term()}.
decode_org(Xml) ->
Ns = [{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'},
{"md", 'urn:oasis:names:tc:SAML:2.0:metadata'}],
esaml_util:threaduntil([
?xpath_text_required("/md:Organization/md:OrganizationName/text()", esaml_org, name, bad_org),
?xpath_text("/md:Organization/md:OrganizationDisplayName/text()", esaml_org, displayname),
?xpath_text("/md:Organization/md:OrganizationURL/text()", esaml_org, url)
], #esaml_org{}).
%% @private
-spec decode_contact(Xml :: #xmlElement{}) -> {ok, #esaml_contact{}} | {error, term()}.
decode_contact(Xml) ->
Ns = [{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'},
{"md", 'urn:oasis:names:tc:SAML:2.0:metadata'}],
esaml_util:threaduntil([
?xpath_text_required("/md:ContactPerson/md:EmailAddress/text()", esaml_contact, email, bad_contact),
?xpath_text("/md:ContactPerson/md:GivenName/text()", esaml_contact, name),
?xpath_text_append("/md:ContactPerson/md:SurName/text()", esaml_contact, name, " ")
], #esaml_contact{}).
%% @private
-spec decode_logout_request(Xml :: #xmlElement{}) -> {ok, #esaml_logoutreq{}} | {error, term()}.
decode_logout_request(Xml) ->
Ns = [{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}],
esaml_util:threaduntil([
?xpath_attr_required("/samlp:LogoutRequest/@Version", esaml_logoutreq, version, bad_version),
?xpath_attr_required("/samlp:LogoutRequest/@IssueInstant", esaml_logoutreq, issue_instant, bad_response),
?xpath_text_required("/samlp:LogoutRequest/saml:NameID/text()", esaml_logoutreq, name, bad_name),
?xpath_attr("/samlp:LogoutRequest/saml:NameID/@SPNameQualifier", esaml_logoutreq, sp_name_qualifier, fun nameid_sp_name_qualifier_map/1),
?xpath_attr("/samlp:LogoutRequest/saml:NameID/@Format", esaml_logoutreq, name_format, fun nameid_format_map/1),
?xpath_attr("/samlp:LogoutRequest/@Destination", esaml_logoutreq, destination),
?xpath_attr("/samlp:LogoutRequest/@Reason", esaml_logoutreq, reason, fun logout_reason_map/1),
?xpath_text("/samlp:LogoutRequest/saml:Issuer/text()", esaml_logoutreq, issuer)
], #esaml_logoutreq{}).
%% @private
-spec decode_logout_response(Xml :: #xmlElement{}) -> {ok, #esaml_logoutresp{}} | {error, term()}.
decode_logout_response(Xml) ->
Ns = [{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}],
esaml_util:threaduntil([
?xpath_attr_required("/samlp:LogoutResponse/@Version", esaml_logoutresp, version, bad_version),
?xpath_attr_required("/samlp:LogoutResponse/@IssueInstant", esaml_logoutresp, issue_instant, bad_response),
?xpath_attr_required("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode/@Value", esaml_logoutresp, status, fun status_code_map/1, bad_response),
?xpath_attr("/samlp:LogoutResponse/@Destination", esaml_logoutresp, destination),
?xpath_text("/samlp:LogoutResponse/saml:Issuer/text()", esaml_logoutresp, issuer)
], #esaml_logoutresp{}).
%% @private
-spec decode_response(Xml :: #xmlElement{}) -> {ok, #esaml_response{}} | {error, term()}.
decode_response(Xml) ->
Ns = [{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}],
esaml_util:threaduntil([
?xpath_attr_required("/samlp:Response/@Version", esaml_response, version, bad_version),
?xpath_attr_required("/samlp:Response/@IssueInstant", esaml_response, issue_instant, bad_response),
?xpath_attr("/samlp:Response/@Destination", esaml_response, destination),
?xpath_text("/samlp:Response/saml:Issuer/text()", esaml_response, issuer),
?xpath_attr("/samlp:Response/samlp:Status/samlp:StatusCode/@Value", esaml_response, status, fun status_code_map/1),
?xpath_recurse("/samlp:Response/saml:Assertion", esaml_response, assertion, decode_assertion)
], #esaml_response{}).
%% @private
-spec decode_assertion(Xml :: #xmlElement{}) -> {ok, #esaml_assertion{}} | {error, term()}.
decode_assertion(Xml) ->
Ns = [{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}],
esaml_util:threaduntil([
?xpath_attr_required("/saml:Assertion/@Version", esaml_assertion, version, bad_version),
?xpath_attr_required("/saml:Assertion/@IssueInstant", esaml_assertion, issue_instant, bad_assertion),
?xpath_attr_required("/saml:Assertion/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData/@Recipient", esaml_assertion, recipient, bad_recipient),
?xpath_text("/saml:Assertion/saml:Issuer/text()", esaml_assertion, issuer),
?xpath_recurse("/saml:Assertion/saml:Subject", esaml_assertion, subject, decode_assertion_subject),
?xpath_recurse("/saml:Assertion/saml:Conditions", esaml_assertion, conditions, decode_assertion_conditions),
?xpath_recurse("/saml:Assertion/saml:AttributeStatement", esaml_assertion, attributes, decode_assertion_attributes),
?xpath_recurse("/saml:Assertion/saml:AuthnStatement", esaml_assertion, authn, decode_assertion_authn)
], #esaml_assertion{}).
-spec decode_assertion_subject(#xmlElement{}) -> {ok, #esaml_subject{}} | {error, term()}.
decode_assertion_subject(Xml) ->
Ns = [{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}],
esaml_util:threaduntil([
?xpath_text("/saml:Subject/saml:NameID/text()", esaml_subject, name),
?xpath_attr("/saml:Subject/saml:NameID/@NameQualifier", esaml_subject, name_qualifier, fun nameid_name_qualifier_map/1),
?xpath_attr("/saml:Subject/saml:NameID/@SPNameQualifier", esaml_subject, sp_name_qualifier, fun nameid_sp_name_qualifier_map/1),
?xpath_attr("/saml:Subject/saml:NameID/@Format", esaml_subject, name_format, fun nameid_format_map/1),
?xpath_attr("/saml:Subject/saml:SubjectConfirmation/@Method", esaml_subject, confirmation_method, fun subject_method_map/1),
?xpath_attr("/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData/@NotOnOrAfter", esaml_subject, notonorafter),
?xpath_attr("/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData/@InResponseTo", esaml_subject, in_response_to)
], #esaml_subject{}).
-spec decode_assertion_conditions(#xmlElement{}) -> {ok, conditions()} | {error, term()}.
decode_assertion_conditions(Xml) ->
Ns = [{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}],
esaml_util:threaduntil([
fun(C) ->
case xmerl_xpath:string("/saml:Conditions/@NotBefore", Xml, [{namespace, Ns}]) of
[#xmlAttribute{value = V}] -> [{not_before, V} | C]; _ -> C
end
end,
fun(C) ->
case xmerl_xpath:string("/saml:Conditions/@NotOnOrAfter", Xml, [{namespace, Ns}]) of
[#xmlAttribute{value = V}] -> [{not_on_or_after, V} | C]; _ -> C
end
end,
fun(C) ->
case xmerl_xpath:string("/saml:Conditions/saml:AudienceRestriction/saml:Audience/text()", Xml, [{namespace, Ns}]) of
[#xmlText{value = V}] -> [{audience, V} | C]; _ -> C
end
end
], []).
-spec decode_assertion_attributes(#xmlElement{}) -> {ok, [{atom(), string()}]} | {error, term()}.
decode_assertion_attributes(Xml) ->
Ns = [{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}],
Attrs = xmerl_xpath:string("/saml:AttributeStatement/saml:Attribute", Xml, [{namespace, Ns}]),
{ok, lists:foldl(fun(AttrElem, In) ->
case [X#xmlAttribute.value || X <- AttrElem#xmlElement.attributes, X#xmlAttribute.name =:= 'Name'] of
[Name] ->
case xmerl_xpath:string("saml:AttributeValue/text()", AttrElem, [{namespace, Ns}]) of
[#xmlText{value = Value}] ->
[{common_attrib_map(Name), Value} | In];
List ->
if (length(List) > 0) ->
Value = [X#xmlText.value || X <- List, element(1, X) =:= xmlText],
[{common_attrib_map(Name), Value} | In];
true ->
In
end
end;
_ -> In
end
end, [], Attrs)}.
-spec decode_assertion_authn(#xmlElement{}) -> {ok, conditions()} | {error, term()}.
decode_assertion_authn(Xml) ->
Ns = [{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}],
esaml_util:threaduntil([
fun(C) ->
case xmerl_xpath:string("/saml:AuthnStatement/@AuthnInstant", Xml, [{namespace, Ns}]) of
[#xmlAttribute{value = V}] -> [{authn_instant, V} | C]; _ -> C
end
end,
fun(C) ->
case xmerl_xpath:string("/saml:AuthnStatement/@SessionNotOnOrAfter", Xml, [{namespace, Ns}]) of
[#xmlAttribute{value = V}] -> [{session_not_on_or_after, V} | C]; _ -> C
end
end,
fun(C) ->
case xmerl_xpath:string("/saml:AuthnStatement/@SessionIndex", Xml, [{namespace, Ns}]) of
[#xmlAttribute{value = V}] -> [{session_index, V} | C]; _ -> C
end
end,
fun(C) ->
case xmerl_xpath:string("/saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef/text()", Xml, [{namespace, Ns}]) of
[#xmlText{value = V}] -> [{authn_context, V} | C]; _ -> C
end
end
], []).
%% @doc Returns the time at which an assertion is considered stale.
%% @private
-spec stale_time(#esaml_assertion{}) -> integer().
stale_time(A) ->
esaml_util:thread([
fun(T) ->
case A#esaml_assertion.subject of
#esaml_subject{notonorafter = ""} -> T;
#esaml_subject{notonorafter = Restrict} ->
Secs = calendar:datetime_to_gregorian_seconds(
esaml_util:saml_to_datetime(Restrict)),
if (Secs < T) -> Secs; true -> T end
end
end,
fun(T) ->
Conds = A#esaml_assertion.conditions,
case proplists:get_value(not_on_or_after, Conds) of
undefined -> T;
Restrict ->
Secs = calendar:datetime_to_gregorian_seconds(
esaml_util:saml_to_datetime(Restrict)),
if (Secs < T) -> Secs; true -> T end
end
end,
fun(T) ->
if (T =:= none) ->
II = A#esaml_assertion.issue_instant,
IISecs = calendar:datetime_to_gregorian_seconds(
esaml_util:saml_to_datetime(II)),
IISecs + 5*60;
true ->
T
end
end
], none).
check_stale(A) ->
Now = erlang:localtime_to_universaltime(erlang:localtime()),
NowSecs = calendar:datetime_to_gregorian_seconds(Now),
T = stale_time(A),
if (NowSecs > T) ->
{error, stale_assertion};
true ->
A
end.
%% @doc Parse and validate an assertion, returning it as a record
%% @private
-spec validate_assertion(AssertionXml :: #xmlElement{}, Recipient :: string(), Audience :: string()) ->
{ok, #esaml_assertion{}} | {error, Reason :: term()}.
validate_assertion(AssertionXml, Recipient, Audience) ->
case decode_assertion(AssertionXml) of
{error, Reason} ->
{error, Reason};
{ok, Assertion} ->
esaml_util:threaduntil([
fun(A) -> case A of
#esaml_assertion{version = "2.0"} -> A;
_ -> {error, bad_version}
end end,
fun(A) -> case A of
#esaml_assertion{recipient = Recipient} -> A;
_ -> {error, bad_recipient}
end end,
fun(A) -> case A of
#esaml_assertion{conditions = Conds} ->
case proplists:get_value(audience, Conds) of
undefined -> A;
Audience -> A;
_ -> {error, bad_audience}
end;
_ -> A
end end,
fun check_stale/1
], Assertion)
end.
%% @doc Produce cloned elements with xml:lang set to represent
%% multi-locale strings.
%% @private
-spec lang_elems(#xmlElement{}, 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}]}].
%% @doc Convert a SAML request/metadata record into XML
%% @private
-spec to_xml(saml_record()) -> #xmlElement{}.
to_xml(#esaml_authnreq{version = V, issue_instant = Time, destination = Dest,
issuer = Issuer, name_format = Format, consumer_location = Consumer}) ->
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:AuthnRequest',
attributes = [#xmlAttribute{name = 'xmlns:samlp', value = proplists:get_value("samlp", Ns#xmlNamespace.nodes)},
#xmlAttribute{name = 'xmlns:saml', value = proplists:get_value("saml", Ns#xmlNamespace.nodes)},
#xmlAttribute{name = 'IssueInstant', value = Time},
#xmlAttribute{name = 'Version', value = V},
#xmlAttribute{name = 'Destination', value = Dest},
#xmlAttribute{name = 'AssertionConsumerServiceURL', value = Consumer},
#xmlAttribute{name = 'ProtocolBinding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"}],
content = [#xmlElement{name = 'saml:Issuer', content = [#xmlText{value = Issuer}]}] ++
case is_list(Format) of
true ->
[#xmlElement{name = 'samlp:NameIDPolicy',
attributes = [#xmlAttribute{name = 'Format', value = Format}]}];
false ->
[]
end
});
to_xml(#esaml_logoutreq{version = V, issue_instant = Time, destination = Dest, issuer = Issuer,
name = NameID, name_qualifier = NameQualifier,
sp_name_qualifier = SPNameQualifier, name_format = NameFormat,
session_index = SessionIndex, reason = Reason}) ->
Ns = #xmlNamespace{nodes = [{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}]},
NameIDAttrs =
case is_list(NameQualifier) of
true -> [#xmlAttribute{name = 'NameQualifier', value = NameQualifier}];
false -> []
end ++
case is_list(SPNameQualifier) of
true -> [#xmlAttribute{name = 'SPNameQualifier', value = SPNameQualifier}];
false -> []
end ++
case is_list(NameFormat) of
true -> [#xmlAttribute{name = 'Format', value = NameFormat}];
false -> []
end,
esaml_util:build_nsinfo(Ns, #xmlElement{name = 'samlp:LogoutRequest',
attributes = [#xmlAttribute{name = 'xmlns:samlp', value = proplists:get_value("samlp", Ns#xmlNamespace.nodes)},
#xmlAttribute{name = 'xmlns:saml', value = proplists:get_value("saml", Ns#xmlNamespace.nodes)},
#xmlAttribute{name = 'IssueInstant', value = Time},
#xmlAttribute{name = 'Version', value = V},
#xmlAttribute{name = 'Destination', value = Dest},
#xmlAttribute{name = 'ProtocolBinding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"},
#xmlAttribute{name = 'Reason', value = rev_logout_reason_map(Reason)}],
content = [
#xmlElement{name = 'saml:Issuer', content = [#xmlText{value = Issuer}]},
#xmlElement{name = 'saml:NameID',
attributes = NameIDAttrs,
content = [#xmlText{value = NameID}]},
#xmlElement{name = 'samlp:SessionIndex', content = [#xmlText{value = SessionIndex}]}
]
});
to_xml(#esaml_logoutresp{version = V, issue_instant = Time,
destination = Dest, issuer = Issuer, status = Status}) ->
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 = proplists:get_value("samlp", Ns#xmlNamespace.nodes)},
#xmlAttribute{name = 'xmlns:saml', value = proplists:get_value("saml", Ns#xmlNamespace.nodes)},
#xmlAttribute{name = 'IssueInstant', value = Time},
#xmlAttribute{name = 'Version', value = V},
#xmlAttribute{name = 'Destination', value = Dest},
#xmlAttribute{name = 'ProtocolBinding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"}],
content = [
#xmlElement{name = 'saml:Issuer', content = [#xmlText{value = Issuer}]},
#xmlElement{name = 'samlp:Status', content = [
#xmlElement{name = 'samlp:StatusCode', content = [
#xmlText{value = rev_status_code_map(Status)}]}]}
]
});
to_xml(#esaml_sp_metadata{org = #esaml_org{name = OrgName, displayname = OrgDisplayName,
url = OrgUrl },
tech = #esaml_contact{name = TechName, email = TechEmail},
signed_requests = SignReq, signed_assertions = SignAss,
certificate = CertBin, cert_chain = CertChain, entity_id = EntityID,
consumer_location = ConsumerLoc,
logout_location = SLOLoc
}) ->
Ns = #xmlNamespace{nodes = [{"md", 'urn:oasis:names:tc:SAML:2.0:metadata'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'},
{"dsig", 'http://www.w3.org/2000/09/xmldsig#'}]},
KeyDescriptorElems = case CertBin of
undefined -> [];
C when is_binary(C) -> [
#xmlElement{name = 'md:KeyDescriptor',
attributes = [#xmlAttribute{name = 'use', value = "signing"}],
content = [#xmlElement{name = 'dsig:KeyInfo',
content = [#xmlElement{name = 'dsig:X509Data',
content =
[#xmlElement{name = 'dsig:X509Certificate',
content = [#xmlText{value = base64:encode_to_string(CertBin)}]} |
[#xmlElement{name = 'dsig:X509Certificate',
content = [#xmlText{value = base64:encode_to_string(CertChainBin)}]} || CertChainBin <- CertChain]]}]}]},
#xmlElement{name = 'md:KeyDescriptor',
attributes = [#xmlAttribute{name = 'use', value = "encryption"}],
content = [#xmlElement{name = 'dsig:KeyInfo',
content = [#xmlElement{name = 'dsig:X509Data',
content =
[#xmlElement{name = 'dsig:X509Certificate',
content = [#xmlText{value = base64:encode_to_string(CertBin)}]} |
[#xmlElement{name = 'dsig:X509Certificate',
content = [#xmlText{value = base64:encode_to_string(CertChainBin)}]} || CertChainBin <- CertChain]]}]}]}
]
end,
SingleLogoutServiceElems = case SLOLoc of
undefined -> [];
_ -> [
#xmlElement{name = 'md:SingleLogoutService',
attributes = [#xmlAttribute{name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-REDIRECT"},
#xmlAttribute{name = 'Location', value = SLOLoc}]},
#xmlElement{name = 'md:SingleLogoutService',
attributes = [#xmlAttribute{name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"},
#xmlAttribute{name = 'Location', value = SLOLoc}]}
]
end,
AssertionConsumerServiceElems = [
#xmlElement{name = 'md:AssertionConsumerService',
attributes = [#xmlAttribute{name = 'isDefault', value = "true"},
#xmlAttribute{name = 'index', value = "0"},
#xmlAttribute{name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"},
#xmlAttribute{name = 'Location', value = ConsumerLoc}]},
#xmlElement{name = 'md:AssertionConsumerService',
attributes = [#xmlAttribute{name = 'index', value = "1"},
#xmlAttribute{name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-REDIRECT"},
#xmlAttribute{name = 'Location', value = ConsumerLoc}]}
],
OrganizationElem = #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)
},
ContactElem = #xmlElement{name = 'md:ContactPerson',
attributes = [#xmlAttribute{name = 'contactType', value = "technical"}],
content = [
#xmlElement{name = 'md:SurName', content = [#xmlText{value = TechName}]},
#xmlElement{name = 'md:EmailAddress', content = [#xmlText{value = TechEmail}]}
]
},
SPSSODescriptorElem = #xmlElement{name = 'md:SPSSODescriptor',
attributes = [#xmlAttribute{name = 'protocolSupportEnumeration', value = "urn:oasis:names:tc:SAML:2.0:protocol"},
#xmlAttribute{name = 'AuthnRequestsSigned', value = atom_to_list(SignReq)},
#xmlAttribute{name = 'WantAssertionsSigned', value = atom_to_list(SignAss)}],
content = KeyDescriptorElems ++ SingleLogoutServiceElems ++ AssertionConsumerServiceElems
},
esaml_util:build_nsinfo(Ns, #xmlElement{
name = 'md:EntityDescriptor',
attributes = [
#xmlAttribute{name = 'xmlns:md', value = atom_to_list(proplists:get_value("md", Ns#xmlNamespace.nodes))},
#xmlAttribute{name = 'xmlns:saml', value = atom_to_list(proplists:get_value("saml", Ns#xmlNamespace.nodes))},
#xmlAttribute{name = 'xmlns:dsig', value = atom_to_list(proplists:get_value("dsig", Ns#xmlNamespace.nodes))},
#xmlAttribute{name = 'entityID', value = EntityID}
], content = [
SPSSODescriptorElem,
OrganizationElem,
ContactElem
]
});
to_xml(_) -> error("unknown record").
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
decode_response_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" IssueInstant=\"2013-01-01T01:01:01Z\" Destination=\"foo\"></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
?assertMatch({ok, #esaml_response{issue_instant = "2013-01-01T01:01:01Z", destination = "foo", status = unknown}}, Resp).
decode_response_no_version_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" IssueInstant=\"2013-01-01T01:01:01Z\" Destination=\"foo\"></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
{error, bad_version} = Resp.
decode_response_no_issue_instant_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" Destination=\"foo\"></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
{error, bad_response} = Resp.
decode_response_destination_optional_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" IssueInstant=\"2013-01-01T01:01:01Z\"></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
{ok, #esaml_response{issue_instant = "2013-01-01T01:01:01Z", status = unknown}} = Resp.
decode_response_status_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" IssueInstant=\"2013-01-01T01:01:01Z\"><saml:Issuer>foo</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" /></samlp:Status></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
{ok, #esaml_response{issue_instant = "2013-01-01T01:01:01Z", status = success, issuer = "foo"}} = Resp.
decode_response_bad_assertion_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" IssueInstant=\"2013-01-01T01:01:01Z\"><saml:Issuer>foo</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" /></samlp:Status><saml:Assertion></saml:Assertion></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
{error, bad_version} = Resp.
decode_assertion_no_recipient_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" IssueInstant=\"2013-01-01T01:01:01Z\"><saml:Issuer>foo</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" /></samlp:Status><saml:Assertion Version=\"2.0\" IssueInstant=\"test\"><saml:Issuer>foo</saml:Issuer><saml:Subject><saml:NameID>foobar</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\" /></saml:Subject></saml:Assertion></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
{error, bad_recipient} = Resp.
decode_assertion_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" IssueInstant=\"2013-01-01T01:01:01Z\"><saml:Issuer>foo</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" /></samlp:Status><saml:Assertion Version=\"2.0\" IssueInstant=\"test\"><saml:Issuer>foo</saml:Issuer><saml:Subject><saml:NameID>foobar</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData Recipient=\"foobar123\" /></saml:SubjectConfirmation></saml:Subject></saml:Assertion></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
{ok, #esaml_response{issue_instant = "2013-01-01T01:01:01Z", issuer = "foo", status = success, assertion = #esaml_assertion{issue_instant = "test", issuer = "foo", recipient = "foobar123", subject = #esaml_subject{name = "foobar", confirmation_method = bearer}}}} = Resp.
decode_conditions_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" IssueInstant=\"2013-01-01T01:01:01Z\"><saml:Issuer>foo</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" /></samlp:Status><saml:Assertion Version=\"2.0\" IssueInstant=\"test\"><saml:Issuer>foo</saml:Issuer><saml:Subject><saml:NameID>foobar</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData Recipient=\"foobar123\" /></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"before\" NotOnOrAfter=\"notafter\"><saml:AudienceRestriction><saml:Audience>foobaraudience</saml:Audience></saml:AudienceRestriction></saml:Conditions></saml:Assertion></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
{ok, #esaml_response{assertion = #esaml_assertion{conditions = Conds}}} = Resp,
[{audience, "foobaraudience"}, {not_before, "before"}, {not_on_or_after, "notafter"}] = lists:sort(Conds).
decode_attributes_test() ->
{Doc, _} = xmerl_scan:string("<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" IssueInstant=\"test\"><saml:Subject><saml:NameID>foobar</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData Recipient=\"foobar123\" /></saml:SubjectConfirmation></saml:Subject><saml:AttributeStatement><saml:Attribute Name=\"urn:oid:0.9.2342.19200300.100.1.3\"><saml:AttributeValue>test@test.com</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"foo\"><saml:AttributeValue>george</saml:AttributeValue><saml:AttributeValue>bar</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"><saml:AttributeValue>test@test.com</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>", [{namespace_conformant, true}]),
Assertion = decode_assertion(Doc),
{ok, #esaml_assertion{attributes = Attrs}} = Assertion,
[{emailaddress, "test@test.com"}, {foo, ["george", "bar"]}, {mail, "test@test.com"}] = lists:sort(Attrs).
decode_solicited_in_response_to_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" IssueInstant=\"2013-01-01T01:01:01Z\"><saml:Issuer>foo</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" /></samlp:Status><saml:Assertion Version=\"2.0\" IssueInstant=\"test\"><saml:Issuer>foo</saml:Issuer><saml:Subject><saml:NameID>foobar</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData Recipient=\"foobar123\" InResponseTo=\"_1234567890\" /></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"before\" NotOnOrAfter=\"notafter\"><saml:AudienceRestriction><saml:Audience>foobaraudience</saml:Audience></saml:AudienceRestriction></saml:Conditions></saml:Assertion></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
{ok, #esaml_response{assertion = #esaml_assertion{subject = #esaml_subject{in_response_to = ReqId}}}} = Resp,
"_1234567890" = ReqId.
decode_unsolicited_in_response_to_test() ->
{Doc, _} = xmerl_scan:string("<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version=\"2.0\" IssueInstant=\"2013-01-01T01:01:01Z\"><saml:Issuer>foo</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" /></samlp:Status><saml:Assertion Version=\"2.0\" IssueInstant=\"test\"><saml:Issuer>foo</saml:Issuer><saml:Subject><saml:NameID>foobar</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData Recipient=\"foobar123\" /></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"before\" NotOnOrAfter=\"notafter\"><saml:AudienceRestriction><saml:Audience>foobaraudience</saml:Audience></saml:AudienceRestriction></saml:Conditions></saml:Assertion></samlp:Response>", [{namespace_conformant, true}]),
Resp = decode_response(Doc),
{ok, #esaml_response{assertion = #esaml_assertion{subject = #esaml_subject{in_response_to = ReqId}}}} = Resp,
"" = ReqId.
validate_assertion_test() ->
Now = erlang:localtime_to_universaltime(erlang:localtime()),
DeathSecs = calendar:datetime_to_gregorian_seconds(Now) + 1,
Death = esaml_util:datetime_to_saml(calendar:gregorian_seconds_to_datetime(DeathSecs)),
Ns = #xmlNamespace{nodes = [{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}]},
E1 = esaml_util:build_nsinfo(Ns, #xmlElement{name = 'saml:Assertion',
attributes = [#xmlAttribute{name = 'xmlns:saml', value = "urn:oasis:names:tc:SAML:2.0:assertion"}, #xmlAttribute{name = 'Version', value = "2.0"}, #xmlAttribute{name = 'IssueInstant', value = "now"}],
content = [
#xmlElement{name = 'saml:Subject', content = [
#xmlElement{name = 'saml:SubjectConfirmation', content = [
#xmlElement{name = 'saml:SubjectConfirmationData',
attributes = [#xmlAttribute{name = 'Recipient', value = "foobar"},
#xmlAttribute{name = 'NotOnOrAfter', value = Death}]
} ]} ]},
#xmlElement{name = 'saml:Conditions', content = [
#xmlElement{name = 'saml:AudienceRestriction', content = [
#xmlElement{name = 'saml:Audience', content = [#xmlText{value = "foo"}]}] }] } ]
}),
{ok, Assertion} = validate_assertion(E1, "foobar", "foo"),
#esaml_assertion{issue_instant = "now", recipient = "foobar", subject = #esaml_subject{notonorafter = Death}, conditions = [{audience, "foo"}]} = Assertion,
{error, bad_recipient} = validate_assertion(E1, "foo", "something"),
{error, bad_audience} = validate_assertion(E1, "foobar", "something"),
E2 = esaml_util:build_nsinfo(Ns, #xmlElement{name = 'saml:Assertion',
attributes = [#xmlAttribute{name = 'xmlns:saml', value = "urn:oasis:names:tc:SAML:2.0:assertion"}, #xmlAttribute{name = 'Version', value = "2.0"}, #xmlAttribute{name = 'IssueInstant', value = "now"}],
content = [
#xmlElement{name = 'saml:Subject', content = [
#xmlElement{name = 'saml:SubjectConfirmation', content = [ ]} ]},
#xmlElement{name = 'saml:Conditions', content = [
#xmlElement{name = 'saml:AudienceRestriction', content = [
#xmlElement{name = 'saml:Audience', content = [#xmlText{value = "foo"}]}] }] } ]
}),
{error, bad_recipient} = validate_assertion(E2, "", "").
validate_stale_assertion_test() ->
Ns = #xmlNamespace{nodes = [{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}]},
OldStamp = esaml_util:datetime_to_saml({{1990,1,1}, {1,1,1}}),
E1 = esaml_util:build_nsinfo(Ns, #xmlElement{name = 'saml:Assertion',
attributes = [#xmlAttribute{name = 'xmlns:saml', value = "urn:oasis:names:tc:SAML:2.0:assertion"}, #xmlAttribute{name = 'Version', value = "2.0"}, #xmlAttribute{name = 'IssueInstant', value = "now"}],
content = [
#xmlElement{name = 'saml:Subject', content = [
#xmlElement{name = 'saml:SubjectConfirmation', content = [
#xmlElement{name = 'saml:SubjectConfirmationData',
attributes = [#xmlAttribute{name = 'Recipient', value = "foobar"},
#xmlAttribute{name = 'NotOnOrAfter', value = OldStamp}]
} ]} ]} ]
}),
{error, stale_assertion} = validate_assertion(E1, "foobar", "foo").
-endif.