Always return authenticationDate, longTermAuthenticationRequestTokenUsed and isFromNewLogin attributes

As specified in the CAS response XML schema (see Appendix A).
Fix #37 as returned attributes are now never empty.
This commit is contained in:
Valentin Samir 2018-04-29 18:48:41 +02:00
parent 4123450e9f
commit ff8373ee6a
7 changed files with 42 additions and 8 deletions

View File

@ -206,7 +206,7 @@ class CASClientV2(CASClientBase, ReturnUnicode):
def parse_attributes_xml_element(cls, element, charset): def parse_attributes_xml_element(cls, element, charset):
attributes = dict() attributes = dict()
for attribute in element: for attribute in element:
tag = cls.self.u(attribute.tag, charset).split(u"}").pop() tag = cls.u(attribute.tag, charset).split(u"}").pop()
if tag in attributes: if tag in attributes:
if isinstance(attributes[tag], list): if isinstance(attributes[tag], list):
attributes[tag].append(cls.u(attribute.text, charset)) attributes[tag].append(cls.u(attribute.text, charset))

View File

@ -29,6 +29,15 @@
</ConfirmationMethod> </ConfirmationMethod>
</SubjectConfirmation> </SubjectConfirmation>
</Subject> </Subject>
<Attribute AttributeName="authenticationDate" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>{{auth_date}}</AttributeValue>
</Attribute>
<Attribute AttributeName="longTermAuthenticationRequestTokenUsed" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>false</AttributeValue>{# we do not support long-term (Remember-Me) auth #}
</Attribute>
<Attribute AttributeName="isFromNewLogin" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>{{is_new_login}}</AttributeValue>
</Attribute>
{% for name, value in attributes %} <Attribute AttributeName="{{name}}" AttributeNamespace="http://www.ja-sig.org/products/cas/"> {% for name, value in attributes %} <Attribute AttributeName="{{name}}" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>{{value}}</AttributeValue> <AttributeValue>{{value}}</AttributeValue>
</Attribute> </Attribute>

View File

@ -2,8 +2,14 @@
<cas:authenticationSuccess> <cas:authenticationSuccess>
<cas:user>{{username}}</cas:user> <cas:user>{{username}}</cas:user>
<cas:attributes> <cas:attributes>
<cas:authenticationDate>{{auth_date}}</cas:authenticationDate>
<cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>{# we do not support long-term (Remember-Me) auth #}
<cas:isFromNewLogin>{{is_new_login}}</cas:isFromNewLogin>
{% for key, value in attributes %} <cas:{{key}}>{{value}}</cas:{{key}}> {% for key, value in attributes %} <cas:{{key}}>{{value}}</cas:{{key}}>
{% endfor %} </cas:attributes> {% endfor %} </cas:attributes>
<cas:attribute name="authenticationDate" value="{{auth_date}}"/>
<cas:attribute name="longTermAuthenticationRequestTokenUsed" value="false"/>
<cas:attribute name="isFromNewLogin" value="{{is_new_login}}"/>
{% for key, value in attributes %} <cas:attribute name="{{key}}" value="{{value}}"/> {% for key, value in attributes %} <cas:attribute name="{{key}}" value="{{value}}"/>
{% endfor %}{% if proxyGrantingTicket %} <cas:proxyGrantingTicket>{{proxyGrantingTicket}}</cas:proxyGrantingTicket> {% endfor %}{% if proxyGrantingTicket %} <cas:proxyGrantingTicket>{{proxyGrantingTicket}}</cas:proxyGrantingTicket>
{% endif %}{% if proxies %} <cas:proxies> {% endif %}{% if proxies %} <cas:proxies>

View File

@ -149,15 +149,23 @@ class XmlContent(object):
namespaces={'cas': "http://www.yale.edu/tp/cas"} namespaces={'cas': "http://www.yale.edu/tp/cas"}
) )
self.assertEqual(len(attributes), 1) self.assertEqual(len(attributes), 1)
ignore_attrs = {"authenticationDate", "longTermAuthenticationRequestTokenUsed", "isFromNewLogin"}
ignored_attrs = 0
attrs1 = set() attrs1 = set()
for attr in attributes[0]: for attr in attributes[0]:
attrs1.add((attr.tag[len("http://www.yale.edu/tp/cas")+2:], attr.text)) name = attr.tag[len("http://www.yale.edu/tp/cas")+2:]
if not name in ignore_attrs:
attrs1.add((name, attr.text))
else:
ignored_attrs += 1
attributes = root.xpath("//cas:attribute", namespaces={'cas': "http://www.yale.edu/tp/cas"}) attributes = root.xpath("//cas:attribute", namespaces={'cas': "http://www.yale.edu/tp/cas"})
self.assertEqual(len(attributes), len(attrs1)) self.assertEqual(len(attributes), len(attrs1) + ignored_attrs)
attrs2 = set() attrs2 = set()
for attr in attributes: for attr in attributes:
attrs2.add((attr.attrib['name'], attr.attrib['value'])) name = attr.attrib['name']
if not name in ignore_attrs:
attrs2.add((name, attr.attrib['value']))
original = set() original = set()
for key, value in original_attributes.items(): for key, value in original_attributes.items():
if isinstance(value, list): if isinstance(value, list):

View File

@ -1907,9 +1907,11 @@ class SamlValidateTestCase(TestCase, BaseServicePattern, XmlContent):
"//samla:AttributeStatement/samla:Attribute", "//samla:AttributeStatement/samla:Attribute",
namespaces={'samla': "urn:oasis:names:tc:SAML:1.0:assertion"} namespaces={'samla': "urn:oasis:names:tc:SAML:1.0:assertion"}
) )
ignore_attrs = {"authenticationDate", "longTermAuthenticationRequestTokenUsed", "isFromNewLogin"} - set(original_attributes.keys())
attrs = set() attrs = set()
for attr in attributes: for attr in attributes:
attrs.add((attr.attrib['AttributeName'], attr.getchildren()[0].text)) if not attr.attrib['AttributeName'] in ignore_attrs:
attrs.add((attr.attrib['AttributeName'], attr.getchildren()[0].text))
original = set() original = set()
for key, value in original_attributes.items(): for key, value in original_attributes.items():
if isinstance(value, list): if isinstance(value, list):

View File

@ -264,7 +264,9 @@ class DummyCAS(BaseHTTPServer.BaseHTTPRequestHandler):
template = loader.get_template('cas_server/serviceValidate.xml') template = loader.get_template('cas_server/serviceValidate.xml')
context = Context({ context = Context({
'username': self.server.username, 'username': self.server.username,
'attributes': self.server.attributes 'attributes': self.server.attributes,
'auth_date': timezone.now().replace(microsecond=0).isoformat(),
'is_new_login': 'true',
}) })
self.wfile.write(return_bytes(template.render(context), "utf8")) self.wfile.write(return_bytes(template.render(context), "utf8"))
else: else:
@ -301,6 +303,8 @@ class DummyCAS(BaseHTTPServer.BaseHTTPRequestHandler):
'ResponseID': utils.gen_saml_id(), 'ResponseID': utils.gen_saml_id(),
'username': self.server.username, 'username': self.server.username,
'attributes': self.server.attributes, 'attributes': self.server.attributes,
'auth_date': timezone.now().replace(microsecond=0).isoformat(),
'is_new_login': 'true',
}) })
self.wfile.write(return_bytes(template.render(context), "utf8")) self.wfile.write(return_bytes(template.render(context), "utf8"))
else: else:

View File

@ -1153,7 +1153,9 @@ class ValidateService(View):
params = { params = {
'username': self.ticket.username(), 'username': self.ticket.username(),
'attributes': self.ticket.attributs_flat(), 'attributes': self.ticket.attributs_flat(),
'proxies': proxies 'proxies': proxies,
'auth_date': self.ticket.user.last_login.replace(microsecond=0).isoformat(),
'is_new_login': 'true' if self.ticket.renew else 'false'
} }
# if pgtUrl is set, require https or localhost # if pgtUrl is set, require https or localhost
if self.pgt_url and ( if self.pgt_url and (
@ -1415,7 +1417,10 @@ class SamlValidate(CsrfExemptView):
'Recipient': self.target, 'Recipient': self.target,
'ResponseID': utils.gen_saml_id(), 'ResponseID': utils.gen_saml_id(),
'username': self.ticket.username(), 'username': self.ticket.username(),
'attributes': self.ticket.attributs_flat() 'attributes': self.ticket.attributs_flat(),
'auth_date': self.ticket.user.last_login.replace(microsecond=0).isoformat(),
'is_new_login': 'true' if self.ticket.renew else 'false'
} }
logger.info( logger.info(
"SamlValidate: ticket %s validated for user %s on service %s." % ( "SamlValidate: ticket %s validated for user %s on service %s." % (