|
| 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('<', '<').replace('>', '>')) |
| 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 | + |
0 commit comments