Skip to content

Commit ea28a54

Browse files
committed
WsTrust
1 parent 861f7fa commit ea28a54

File tree

4 files changed

+407
-0
lines changed

4 files changed

+407
-0
lines changed

msal/wstrust_request.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#------------------------------------------------------------------------------
2+
#
3+
# Copyright (c) Microsoft Corporation.
4+
# All rights reserved.
5+
#
6+
# This code is licensed under the MIT License.
7+
#
8+
# Permission is hereby granted, free of charge, to any person obtaining a copy
9+
# of this software and associated documentation files(the "Software"), to deal
10+
# in the Software without restriction, including without limitation the rights
11+
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
12+
# copies of the Software, and to permit persons to whom the Software is
13+
# furnished to do so, subject to the following conditions :
14+
#
15+
# The above copyright notice and this permission notice shall be included in
16+
# all copies or substantial portions of the Software.
17+
#
18+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
21+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
# THE SOFTWARE.
25+
#
26+
#------------------------------------------------------------------------------
27+
28+
import uuid
29+
from datetime import datetime, timedelta
30+
import re
31+
import logging
32+
33+
import requests
34+
35+
from .mex import Mex
36+
import wstrust_response
37+
38+
39+
logger = logging.getLogger(__file__)
40+
41+
def send_request(
42+
username, password, cloud_audience_urn, endpoint_address, soap_action,
43+
**kwargs):
44+
if not endpoint_address:
45+
raise ValueError("WsTrust endpoint address can not be empty")
46+
if soap_action is None:
47+
wstrust2005_regex = r'[/trust]?[2005][/usernamemixed]?'
48+
wstrust13_regex = r'[/trust]?[13][/usernamemixed]?'
49+
if re.search(wstrust2005_regex, endpoint_address):
50+
soap_action = Mex.ACTION_2005
51+
elif re.search(wstrust13_regex, endpoint_address):
52+
soap_action = Mex.ACTION_13
53+
assert soap_action in (Mex.ACTION_13, Mex.ACTION_2005) # A loose check here
54+
data = _build_rst(
55+
username, password, cloud_audience_urn, endpoint_address, soap_action)
56+
resp = requests.post(endpoint_address, data=data, headers={
57+
'Content-type':'application/soap+xml; charset=utf-8',
58+
'SOAPAction': soap_action,
59+
}, **kwargs)
60+
if resp.status_code >= 400:
61+
logger.debug("Unsuccessful WsTrust request receives: %s", resp.text)
62+
# It turns out ADFS uses 5xx status code even with client-side incorrect password error
63+
# resp.raise_for_status()
64+
return wstrust_response.parse_response(resp.text)
65+
66+
def escape_password(password):
67+
return (password.replace('&', '&').replace('"', '"')
68+
.replace("'", ''') # the only one not provided by cgi.escape(s, True)
69+
.replace('<', '&lt;').replace('>', '&gt;'))
70+
71+
def wsu_time_format(datetime_obj):
72+
# WsTrust (http://docs.oasis-open.org/ws-sx/ws-trust/v1.4/ws-trust.html)
73+
# does not seem to define timestamp format, but we see YYYY-mm-ddTHH:MM:SSZ
74+
# here (https://www.ibm.com/developerworks/websphere/library/techarticles/1003_chades/1003_chades.html)
75+
# It avoids the uncertainty of the optional ".ssssss" in datetime.isoformat()
76+
# https://docs.python.org/2/library/datetime.html#datetime.datetime.isoformat
77+
return datetime_obj.strftime('%Y-%m-%dT%H:%M:%SZ')
78+
79+
def _build_rst(username, password, cloud_audience_urn, endpoint_address, soap_action):
80+
now = datetime.utcnow()
81+
return """<s:Envelope xmlns:s='{s}' xmlns:wsa='{wsa}' xmlns:wsu='{wsu}'>
82+
<s:Header>
83+
<wsa:Action s:mustUnderstand='1'>{soap_action}</wsa:Action>
84+
<wsa:messageID>urn:uuid:{message_id}</wsa:messageID>
85+
<wsa:ReplyTo>
86+
<wsa:Address>http://www.w3.org/2005/08/addressing/anonymous</wsa:Address>
87+
</wsa:ReplyTo>
88+
<wsa:To s:mustUnderstand='1'>{endpoint_address}</wsa:To>
89+
90+
<wsse:Security s:mustUnderstand='1'
91+
xmlns:wsse='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'>
92+
<wsu:Timestamp wsu:Id='_0'>
93+
<wsu:Created>{time_now}</wsu:Created>
94+
<wsu:Expires>{time_expire}</wsu:Expires>
95+
</wsu:Timestamp>
96+
<wsse:UsernameToken wsu:Id='ADALUsernameToken'>
97+
<wsse:Username>{username}</wsse:Username>
98+
<wsse:Password>{password}</wsse:Password>
99+
</wsse:UsernameToken>
100+
</wsse:Security>
101+
102+
</s:Header>
103+
<s:Body>
104+
<wst:RequestSecurityToken xmlns:wst='{wst}'>
105+
<wsp:AppliesTo xmlns:wsp='http://schemas.xmlsoap.org/ws/2004/09/policy'>
106+
<wsa:EndpointReference>
107+
<wsa:Address>{applies_to}</wsa:Address>
108+
</wsa:EndpointReference>
109+
</wsp:AppliesTo>
110+
<wst:KeyType>{key_type}</wst:KeyType>
111+
<wst:RequestType>{request_type}</wst:RequestType>
112+
</wst:RequestSecurityToken>
113+
</s:Body>
114+
</s:Envelope>""".format(
115+
s=Mex.NS["s"], wsu=Mex.NS["wsu"], wsa=Mex.NS["wsa10"],
116+
soap_action=soap_action, message_id=str(uuid.uuid4()),
117+
endpoint_address=endpoint_address,
118+
time_now=wsu_time_format(now),
119+
time_expire=wsu_time_format(now + timedelta(minutes=10)),
120+
username=username, password=escape_password(password),
121+
wst=Mex.NS["wst"] if soap_action == Mex.ACTION_13 else Mex.NS["wst2005"],
122+
applies_to=cloud_audience_urn,
123+
key_type='http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer'
124+
if soap_action == Mex.ACTION_13 else
125+
'http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey',
126+
request_type='http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue'
127+
if soap_action == Mex.ACTION_13 else
128+
'http://schemas.xmlsoap.org/ws/2005/02/trust/Issue',
129+
)
130+

msal/wstrust_response.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#------------------------------------------------------------------------------
2+
#
3+
# Copyright (c) Microsoft Corporation.
4+
# All rights reserved.
5+
#
6+
# This code is licensed under the MIT License.
7+
#
8+
# Permission is hereby granted, free of charge, to any person obtaining a copy
9+
# of this software and associated documentation files(the "Software"), to deal
10+
# in the Software without restriction, including without limitation the rights
11+
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
12+
# copies of the Software, and to permit persons to whom the Software is
13+
# furnished to do so, subject to the following conditions :
14+
#
15+
# The above copyright notice and this permission notice shall be included in
16+
# all copies or substantial portions of the Software.
17+
#
18+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
21+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
# THE SOFTWARE.
25+
#
26+
#------------------------------------------------------------------------------
27+
28+
try:
29+
from xml.etree import cElementTree as ET
30+
except ImportError:
31+
from xml.etree import ElementTree as ET
32+
import re
33+
34+
from .mex import Mex
35+
36+
37+
SAML_TOKEN_TYPE_V1 = 'urn:oasis:names:tc:SAML:1.0:assertion'
38+
SAML_TOKEN_TYPE_V2 = 'urn:oasis:names:tc:SAML:2.0:assertion'
39+
40+
def parse_response(body): # Returns {"token": "<saml:assertion ...>", "type": "..."}
41+
token = parse_token_by_re(body)
42+
if token:
43+
return token
44+
error = parse_error(body)
45+
raise RuntimeError("WsTrust server returned error in RSTR: %s" % (error or body))
46+
47+
def parse_error(body): # Returns error as a dict. See unit test case for an example.
48+
dom = ET.fromstring(body)
49+
reason_text_node = dom.find('s:Body/s:Fault/s:Reason/s:Text', Mex.NS)
50+
subcode_value_node = dom.find('s:Body/s:Fault/s:Code/s:Subcode/s:Value', Mex.NS)
51+
if reason_text_node is not None or subcode_value_node is not None:
52+
return {"reason": reason_text_node.text, "code": subcode_value_node.text}
53+
54+
def findall_content(xml_string, tag):
55+
"""
56+
Given a tag name without any prefix,
57+
this function returns a list of the raw content inside this tag as-is.
58+
59+
>>> findall_content("<ns0:foo> what <bar> ever </bar> content </ns0:foo>", "foo")
60+
[" what <bar> ever </bar> content "]
61+
62+
Motivation:
63+
64+
Usually we would use XML parser to extract the data by xpath.
65+
However the ElementTree in Python will implicitly normalize the output
66+
by "hoisting" the inner inline namespaces into the outmost element.
67+
The result will be a semantically equivalent XML snippet,
68+
but not fully identical to the original one.
69+
While this effect shouldn't become a problem in all other cases,
70+
it does not seem to fully comply with Exclusive XML Canonicalization spec
71+
(https://www.w3.org/TR/xml-exc-c14n/), and void the SAML token signature.
72+
SAML signature algo needs the "XML -> C14N(XML) -> Signed(C14N(Xml))" order.
73+
74+
The binary extention lxml is probably the canonical way to solve this
75+
(https://stackoverflow.com/questions/22959577/python-exclusive-xml-canonicalization-xml-exc-c14n)
76+
but here we use this workaround, based on Regex, to return raw content as-is.
77+
"""
78+
# \w+ is good enough for https://www.w3.org/TR/REC-xml/#NT-NameChar
79+
pattern = r"<(?:\w+:)?%(tag)s(?:[^>]*)>(.*)</(?:\w+:)?%(tag)s" % {"tag": tag}
80+
return re.findall(pattern, xml_string, re.DOTALL)
81+
82+
def parse_token_by_re(raw_response): # Returns the saml:assertion
83+
for rstr in findall_content(raw_response, "RequestSecurityTokenResponse"):
84+
token_types = findall_content(rstr, "TokenType")
85+
tokens = findall_content(rstr, "RequestedSecurityToken")
86+
if token_types and tokens:
87+
assert token_types[0] in (SAML_TOKEN_TYPE_V1, SAML_TOKEN_TYPE_V2)
88+
return {"token": tokens[0].encode('us-ascii'), "type": token_types[0]}
89+

tests/rst_response.xml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
2+
<s:Header>
3+
<a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal</a:Action>
4+
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
5+
<u:Timestamp u:Id="_0">
6+
<u:Created>2013-11-15T03:08:25.221Z</u:Created>
7+
<u:Expires>2013-11-15T03:13:25.221Z</u:Expires>
8+
</u:Timestamp>
9+
</o:Security>
10+
</s:Header>
11+
<s:Body>
12+
<trust:RequestSecurityTokenResponseCollection xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
13+
<trust:RequestSecurityTokenResponse>
14+
<trust:Lifetime>
15+
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2013-11-15T03:08:25.205Z</wsu:Created>
16+
<wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2013-11-15T04:08:25.205Z</wsu:Expires>
17+
</trust:Lifetime>
18+
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
19+
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
20+
<wsa:Address>https://login.microsoftonline.com/extSTS.srf</wsa:Address>
21+
</wsa:EndpointReference>
22+
</wsp:AppliesTo>
23+
<trust:RequestedSecurityToken>
24+
<saml:Assertion AssertionID="_9bd2b280-f153-471a-9b73-c1df0d555075" IssueInstant="2013-11-15T03:08:25.221Z" Issuer="http://fs.richard-randall.com/adfs/services/trust" MajorVersion="1" MinorVersion="1" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion">
25+
<saml:Conditions NotBefore="2013-11-15T03:08:25.205Z" NotOnOrAfter="2013-11-15T04:08:25.205Z">
26+
<saml:AudienceRestrictionCondition>
27+
<saml:Audience>https://login.microsoftonline.com/extSTS.srf</saml:Audience>
28+
</saml:AudienceRestrictionCondition>
29+
</saml:Conditions>
30+
<saml:AttributeStatement>
31+
<saml:Subject>
32+
<saml:NameIdentifier Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">1TIu064jGEmmf+hnI+F0Jg==</saml:NameIdentifier>
33+
<saml:SubjectConfirmation>
34+
<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
35+
</saml:SubjectConfirmation>
36+
</saml:Subject>
37+
<saml:Attribute AttributeName="UPN" AttributeNamespace="http://schemas.xmlsoap.org/claims">
38+
<saml:AttributeValue>[email protected]</saml:AttributeValue>
39+
</saml:Attribute>
40+
<saml:Attribute AttributeName="ImmutableID" AttributeNamespace="http://schemas.microsoft.com/LiveID/Federation/2008/05">
41+
<saml:AttributeValue>1TIu064jGEmmf+hnI+F0Jg==</saml:AttributeValue>
42+
</saml:Attribute>
43+
</saml:AttributeStatement>
44+
<saml:AuthenticationStatement AuthenticationInstant="2013-11-15T03:08:25.174Z" AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password">
45+
<saml:Subject>
46+
<saml:NameIdentifier Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">1TIu064jGEmmf+hnI+F0Jg==</saml:NameIdentifier>
47+
<saml:SubjectConfirmation>
48+
<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
49+
</saml:SubjectConfirmation>
50+
</saml:Subject>
51+
</saml:AuthenticationStatement>
52+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
53+
<ds:SignedInfo>
54+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
55+
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
56+
<ds:Reference URI="#_9bd2b280-f153-471a-9b73-c1df0d555075">
57+
<ds:Transforms>
58+
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
59+
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
60+
</ds:Transforms>
61+
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
62+
<ds:DigestValue>3i95D+nRbsyRitSPeT7ZtEr5vbM=</ds:DigestValue>
63+
</ds:Reference>
64+
</ds:SignedInfo>
65+
<ds:SignatureValue>aVNmmKLNdAlBxxcNciWVfxynZUPR9ql8ZZSZt/qpqL/GB3HX/cL/QnfG2OOKrmhgEaR0Ul4grZhGJxlxMPDL0fhnBz+VJ5HwztMFgMYs3Md8A2sZd9n4dfu7+CByAna06lCwwfdFWlNV1MBFvlWvYtCLNkpYVr/aglmb9zpMkNxEOmHe/cwxUtYlzH4RpIsIT5pruoJtUxKcqTRDEeeYdzjBAiJuguQTChLmHNoMPdX1RmtJlPsrZ1s9R/IJky7fHLjB7jiTDceRCS5QUbgUqYbLG1MjFXthY2Hr7K9kpYjxxIk6xmM7mFQE3Hts3bj6UU7ElUvHpX9bxxk3pqzlhg==</ds:SignatureValue>
66+
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
67+
<X509Data>
68+
<X509Certificate>MIIC6DCCAdCgAwIBAgIQaztYF2TpvZZG6yreA3NRpzANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVBREZTIFNpZ25pbmcgLSBmcy5yaWNoYXJkLXJhbmRhbGwuY29tMB4XDTEzMTExMTAzNTMwMFoXDTE0MTExMTAzNTMwMFowMDEuMCwGA1UEAxMlQURGUyBTaWduaW5nIC0gZnMucmljaGFyZC1yYW5kYWxsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO+1VWY/sYDdN3hdsvT+mWHTcOwjp2G9e0AEZdmgh7bS54WUJw9y0cMxJmGB0jAAW40zomzIbS8/o3iuxcJyFgBVtMFfXwFjVQJnZJ7IMXFs1V/pJHrwWHxePz/WzXFtMaqEIe8QummJ07UBg9UsYZUYTGO9NDGw1Yr/oRNsl7bLA0S/QlW6yryf6l3snHzIgtO2xiWn6q3vCJTTVNMROkI2YKNKdYiD5fFD77kFACfJmOwP8MN9u+HM2IN6g0Nv5s7rMyw077Co/xKefamWQCB0jLpv89jo3hLgkwIgWX4cMVgHSNmdzXSgC3owG8ivRuJDATh83GiqI6jzA1+x4rkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAxA5MQZHw9lJYDpU4f45EYrWPEaAPnncaoxIeLE9fG14gA01frajRfdyoO0AKqb+ZG6sePKngsuq4QHA2EnEI4Di5uWKsXy1Id0AXUSUhLpe63alZ8OwiNKDKn71nwpXnlGwKqljnG3xBMniGtGKrFS4WM+joEHzaKpvgtGRGoDdtXF4UXZJcn2maw6d/kiHrQ3kWoQcQcJ9hVIo8bC0BPvxV0Qh4TF3Nb3tKhaXsY68eMxMGbHok9trVHQ3Vew35FuTg1JzsfCFSDF8sxu7FJ4iZ7VLM8MQLnvIMcubLJvc57EHSsNyeiqBFQIYkdg7MSf+Ot2qJjfExgo+NOtWN+g==</X509Certificate>
69+
</X509Data>
70+
</KeyInfo>
71+
</ds:Signature>
72+
</saml:Assertion>
73+
</trust:RequestedSecurityToken>
74+
<trust:RequestedAttachedReference>
75+
<o:SecurityTokenReference k:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" xmlns:k="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
76+
<o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">_9bd2b280-f153-471a-9b73-c1df0d555075</o:KeyIdentifier>
77+
</o:SecurityTokenReference>
78+
</trust:RequestedAttachedReference>
79+
<trust:RequestedUnattachedReference>
80+
<o:SecurityTokenReference k:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" xmlns:k="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
81+
<o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">_9bd2b280-f153-471a-9b73-c1df0d555075</o:KeyIdentifier>
82+
</o:SecurityTokenReference>
83+
</trust:RequestedUnattachedReference>
84+
<trust:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</trust:TokenType>
85+
<trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
86+
<trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
87+
</trust:RequestSecurityTokenResponse>
88+
</trust:RequestSecurityTokenResponseCollection>
89+
</s:Body>
90+
</s:Envelope>

0 commit comments

Comments
 (0)