Skip to content

Commit 991e9ae

Browse files
committed
More unit tests for List and ListItem resources, bug fixes (#171), initial support for Federated Authentication with SharePoint Online
1 parent 1ac2c73 commit 991e9ae

File tree

13 files changed

+259
-100
lines changed

13 files changed

+259
-100
lines changed

examples/sharepoint/web_read_direct.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from settings import settings
44

55
from office365.runtime.auth.authentication_context import AuthenticationContext
6-
from office365.runtime.client_request import ClientRequest
76
from office365.runtime.utilities.request_options import RequestOptions
87
from office365.sharepoint.client_context import ClientContext
98

109
if __name__ == '__main__':
10+
1111
context_auth = AuthenticationContext(url=settings['url'])
1212
if context_auth.acquire_token_for_user(username=settings['user_credentials']['username'],
1313
password=settings['user_credentials']['password']):
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wssc="http://schemas.xmlsoap.org/ws/2005/02/sc" xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust">
3+
<s:Header>
4+
<wsa:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
5+
<wsa:To s:mustUnderstand="1">{auth_url}</wsa:To>
6+
<wsa:MessageID>{message_id}</wsa:MessageID>
7+
<wsse:Security>
8+
<wsse:UsernameToken wsu:Id="user">
9+
<wsse:Username>{username}</wsse:Username>
10+
<wsse:Password>{password}</wsse:Password>
11+
</wsse:UsernameToken>
12+
</wsse:Security>
13+
</s:Header>
14+
<s:Body>
15+
<wst:RequestSecurityToken Id="RST0">
16+
<wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
17+
<wsp:AppliesTo>
18+
<wsa:EndpointReference>
19+
<wsa:Address>urn:federation:MicrosoftOnline</wsa:Address>
20+
</wsa:EndpointReference>
21+
</wsp:AppliesTo>
22+
<wst:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</wst:KeyType>
23+
</wst:RequestSecurityToken>
24+
</s:Body>
25+
</s:Envelope>

office365/runtime/auth/RST2.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<S:Envelope xmlns:S="http://www.w3.org/2003/05/soap-envelope"
3+
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
4+
xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
5+
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
6+
xmlns:wsa="http://www.w3.org/2005/08/addressing"
7+
xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust">
8+
<S:Header>
9+
<wsa:Action S:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
10+
<wsa:To S:mustUnderstand="1">https://login.microsoftonline.com/rst2.srf</wsa:To>
11+
<ps:AuthInfo xmlns:ps="http://schemas.microsoft.com/LiveID/SoapServices/v1" Id="PPAuthInfo">
12+
<ps:BinaryVersion>5</ps:BinaryVersion>
13+
<ps:HostingApp>Managed IDCRL</ps:HostingApp>
14+
</ps:AuthInfo>
15+
<wsse:Security></wsse:Security>
16+
</S:Header>
17+
<S:Body>
18+
<wst:RequestSecurityToken xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust" Id="RST0">
19+
<wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
20+
<wsp:AppliesTo>
21+
<wsa:EndpointReference>
22+
<wsa:Address>sharepoint.com</wsa:Address>
23+
</wsa:EndpointReference>
24+
</wsp:AppliesTo>
25+
<wsp:PolicyReference URI="MBI"></wsp:PolicyReference>
26+
</wst:RequestSecurityToken>
27+
</S:Body>
28+
</S:Envelope>

office365/runtime/auth/SAML.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
<a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>
88
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
99
<o:UsernameToken>
10-
<o:Username>[username]</o:Username>
11-
<o:Password>[password]</o:Password>
10+
<o:Username>{username}</o:Username>
11+
<o:Password>{password}</o:Password>
1212
</o:UsernameToken>
1313
</o:Security>
1414
</s:Header>
1515
<s:Body>
1616
<t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
1717
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
1818
<a:EndpointReference>
19-
<a:Address>[endpoint]</a:Address>
19+
<a:Address>{auth_url}</a:Address>
2020
</a:EndpointReference>
2121
</wsp:AppliesTo>
2222
<wsp:PolicyReference xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" URI="MBI"></wsp:PolicyReference>
Lines changed: 138 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,78 @@
11
import os
2+
import uuid
23
from xml.etree import ElementTree
3-
44
import requests
55
import requests.utils
6-
76
import office365.logger
87
from office365.runtime.auth.base_token_provider import BaseTokenProvider
8+
from office365.runtime.auth.sts_info import STSInfo
9+
from office365.runtime.auth.user_realm_info import UserRealmInfo
910

1011
office365.logger.ensure_debug_secrets()
1112

1213

1314
class SamlTokenProvider(BaseTokenProvider, office365.logger.LoggerContext):
14-
""" SAML Security Token Service for O365"""
15-
16-
def __init__(self, url, username, password):
17-
18-
self.url = url
19-
self.username = username
20-
self.password = password
21-
22-
# External Security Token Service for SPO
23-
self.sts = {
24-
'host': 'login.microsoftonline.com',
25-
'path': '/extSTS.srf'
26-
}
27-
28-
# Sign in page url
29-
self.login = '/_forms/default.aspx?wa=wsignin1.0'
15+
"""SAML Security Token Service provider"""
3016

17+
def __init__(self, authority_url, username, password):
18+
self.__username = username
19+
self.__password = password
20+
# Security Token Service info
21+
self.sts = STSInfo(authority_url)
3122
# Last occurred error
3223
self.error = ''
33-
34-
self.token = None
3524
self.FedAuth = None
3625
self.rtFa = None
26+
self._auth_cookies = []
27+
self.__ns_prefixes = {
28+
'S': '{http://www.w3.org/2003/05/soap-envelope}',
29+
's': '{http://www.w3.org/2003/05/soap-envelope}',
30+
'psf': '{http://schemas.microsoft.com/Passport/SoapServices/SOAPFault}',
31+
'wst': '{http://schemas.xmlsoap.org/ws/2005/02/trust}',
32+
'wsse': '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}',
33+
'saml': '{urn:oasis:names:tc:SAML:1.0:assertion}',
34+
'u': '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd}',
35+
'wsa': '{http://www.w3.org/2005/08/addressing}',
36+
'wsp': '{http://schemas.xmlsoap.org/ws/2004/09/policy}',
37+
'ps': '{http://schemas.microsoft.com/LiveID/SoapServices/v1}',
38+
'ds': '{http://www.w3.org/2000/09/xmldsig#}'
39+
}
40+
for key in self.__ns_prefixes.keys():
41+
ElementTree.register_namespace(key, self.__ns_prefixes[key][1:-1])
3742

38-
def acquire_token(self):
39-
"""Acquire user token"""
43+
def acquire_token(self, **kwargs):
44+
"""Acquire user token
45+
"""
4046
logger = self.logger(self.acquire_token.__name__)
41-
logger.debug('called')
47+
logger.debug('acquire_token called')
4248

4349
try:
4450
logger.debug("Acquiring Access Token..")
45-
try:
46-
from urlparse import urlparse # Python 2.X
47-
except ImportError:
48-
from urllib.parse import urlparse # Python 3+
49-
url = urlparse(self.url)
50-
options = {
51-
'username': self.username,
52-
'password': self.password,
53-
'sts': self.sts,
54-
'endpoint': url.scheme + '://' + url.hostname + self.login
55-
}
56-
57-
self.acquire_service_token(options)
58-
self.acquire_authentication_cookie(options)
51+
user_realm = self.get_user_realm(self.__username)
52+
if user_realm.IsFederated:
53+
token = self.acquire_service_token_from_adfs(user_realm.STSAuthUrl, self.__username, self.__password)
54+
else:
55+
token = self.acquire_service_token(self.__username, self.__password)
56+
self.acquire_authentication_cookie(token)
5957
return True
6058
except requests.exceptions.RequestException as e:
6159
self.error = "Error: {}".format(e)
6260
return False
6361

62+
def get_user_realm(self, login):
63+
"""Get User Realm"""
64+
response = requests.post(self.sts.userRealmServiceUrl, data="login={0}&xml=1".format(login),
65+
headers={'Content-Type': 'application/x-www-form-urlencoded'})
66+
xml = ElementTree.fromstring(response.content)
67+
node = xml.find('NameSpaceType')
68+
if node is not None:
69+
if node.text == 'Federated':
70+
info = UserRealmInfo(xml.find('STSAuthURL').text, True)
71+
else:
72+
info = UserRealmInfo(None, False)
73+
return info
74+
return None
75+
6476
def get_authentication_cookie(self):
6577
"""Generate Auth Cookie"""
6678
logger = self.logger(self.get_authentication_cookie.__name__)
@@ -71,62 +83,112 @@ def get_authentication_cookie(self):
7183
def get_last_error(self):
7284
return self.error
7385

74-
def acquire_service_token(self, options):
86+
def acquire_service_token_from_adfs(self, adfs_url, username, password):
87+
logger = self.logger(self.acquire_service_token_from_adfs.__name__)
88+
payload = self._prepare_request_from_template('FederatedSAML.xml', {
89+
'auth_url': adfs_url,
90+
'username': username,
91+
'password': password,
92+
'message_id': str(uuid.uuid4()),
93+
'created': self.sts.created,
94+
'expires': self.sts.expires,
95+
'issuer': self.sts.federationTokenIssuer
96+
})
97+
response = requests.post(adfs_url, data=payload,
98+
headers={'Content-Type': 'application/soap+xml; charset=utf-8'})
99+
try:
100+
xml = ElementTree.fromstring(response.content)
101+
# 1.find assertion
102+
assertion_node = xml.find(
103+
'{0}Body/{1}RequestSecurityTokenResponse/{1}RequestedSecurityToken/{2}Assertion'.format(
104+
self.__ns_prefixes['s'], self.__ns_prefixes['wst'], self.__ns_prefixes['saml']))
105+
if assertion_node is None:
106+
self.error = 'Cannot get security assertion for user {0} from {1}'.format(self.__username, adfs_url)
107+
logger.error(self.error)
108+
return None
109+
# 2. prepare & submit token request
110+
self.sts.securityTokenServicePath = 'rst2.srf'
111+
template = self._prepare_request_from_template('RST2.xml', {
112+
'auth_url': self.sts.authorityUrl,
113+
'serviceTokenUrl': self.sts.securityTokenServiceUrl
114+
})
115+
template_xml = ElementTree.fromstring(template)
116+
security_node = template_xml.find(
117+
'{0}Header/{1}Security'.format(self.__ns_prefixes['s'], self.__ns_prefixes['wsse']))
118+
119+
security_node.insert(1, assertion_node)
120+
payload = ElementTree.tostring(template_xml).decode()
121+
# 3. get token
122+
response = requests.post(self.sts.securityTokenServiceUrl, data=payload,
123+
headers={'Content-Type': 'application/soap+xml'})
124+
token = self._process_service_token_response(response)
125+
logger.debug_secrets('security token: %s', token)
126+
return token
127+
except ElementTree.ParseError as e:
128+
self.error = 'An error occurred while parsing the server response: {}'.format(e)
129+
logger.error(self.error)
130+
return None
131+
132+
def acquire_service_token(self, username, password, service_target=None, service_policy=None):
75133
"""Retrieve service token"""
76134
logger = self.logger(self.acquire_service_token.__name__)
77-
logger.debug_secrets('options: %s', options)
78-
79-
request_body = self.prepare_security_token_request({
80-
'username': options['username'],
81-
'password': options['password'],
82-
'endpoint': self.url
135+
payload = self._prepare_request_from_template('SAML.xml', {
136+
'auth_url': self.sts.authorityUrl,
137+
'username': username,
138+
'password': password,
139+
'message_id': str(uuid.uuid4()),
140+
'created': self.sts.created,
141+
'expires': self.sts.expires,
142+
'issuer': self.sts.federationTokenIssuer
83143
})
84-
85-
sts_url = 'https://' + options['sts']['host'] + options['sts']['path']
86-
response = requests.post(sts_url, data=request_body,
144+
logger.debug_secrets('options: %s', payload)
145+
response = requests.post(self.sts.securityTokenServiceUrl, data=payload,
87146
headers={'Content-Type': 'application/x-www-form-urlencoded'})
88-
token = self.process_service_token_response(response)
89-
logger.debug_secrets('token: %s', token)
90-
if token:
91-
self.token = token
92-
return True
93-
return False
147+
token = self._process_service_token_response(response)
148+
logger.debug_secrets('security token: %s', token)
149+
return token
94150

95-
def process_service_token_response(self, response):
96-
logger = self.logger(self.process_service_token_response.__name__)
151+
def _process_service_token_response(self, response):
152+
logger = self.logger(self._process_service_token_response.__name__)
97153
logger.debug_secrets('response: %s\nresponse.content: %s', response, response.content)
98154

99-
xml = ElementTree.fromstring(response.content)
100-
ns_prefixes = {'S': '{http://www.w3.org/2003/05/soap-envelope}',
101-
'psf': '{http://schemas.microsoft.com/Passport/SoapServices/SOAPFault}',
102-
'wst': '{http://schemas.xmlsoap.org/ws/2005/02/trust}',
103-
'wsse': '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}'}
104-
logger.debug_secrets("ns_prefixes: %s", ns_prefixes)
155+
try:
156+
xml = ElementTree.fromstring(response.content)
157+
except ElementTree.ParseError as e:
158+
self.error = 'An error occurred while parsing the server response: {}'.format(e)
159+
logger.error(self.error)
160+
return None
105161

106162
# check for errors
107-
if xml.find('{0}Body/{0}Fault'.format(ns_prefixes['S'])) is not None:
108-
error = xml.find('{0}Body/{0}Fault/{0}Detail/{1}error/{1}internalerror/{1}text'.format(ns_prefixes['S'],
109-
ns_prefixes['psf']))
110-
self.error = 'An error occurred while retrieving token: {0}'.format(error.text)
163+
if xml.find('{0}Body/{0}Fault'.format(self.__ns_prefixes['s'])) is not None:
164+
error = xml.find('{0}Body/{0}Fault/{0}Detail/{1}error/{1}internalerror/{1}text'.format(self.__ns_prefixes['s'],
165+
self.__ns_prefixes['psf']))
166+
if error is None:
167+
self.error = 'An error occurred while retrieving token from XML response.'
168+
else:
169+
self.error = 'An error occurred while retrieving token from XML response: {0}'.format(error.text)
111170
logger.error(self.error)
112171
return None
113172

114173
# extract token
115174
token = xml.find(
116175
'{0}Body/{1}RequestSecurityTokenResponse/{1}RequestedSecurityToken/{2}BinarySecurityToken'.format(
117-
ns_prefixes['S'], ns_prefixes['wst'], ns_prefixes['wsse']))
176+
self.__ns_prefixes['s'], self.__ns_prefixes['wst'], self.__ns_prefixes['wsse']))
177+
if token is None:
178+
self.error = 'Cannot get binary security token for from {0}'.format(self.sts.securityTokenServiceUrl)
179+
logger.error(self.error)
180+
return None
118181
logger.debug_secrets("token: %s", token)
119182
return token.text
120183

121-
def acquire_authentication_cookie(self, options):
184+
def acquire_authentication_cookie(self, security_token):
122185
"""Retrieve SPO auth cookie"""
123186
logger = self.logger(self.acquire_authentication_cookie.__name__)
124-
125-
url = options['endpoint']
126-
127187
session = requests.session()
128-
logger.debug_secrets("session: %s\nsession.post(%s, data=%s)", session, url, self.token)
129-
session.post(url, data=self.token, headers={'Content-Type': 'application/x-www-form-urlencoded'})
188+
logger.debug_secrets("session: %s\nsession.post(%s, data=%s)", session, self.sts.signInPageUrl,
189+
security_token)
190+
session.post(self.sts.signInPageUrl, data=security_token,
191+
headers={'Content-Type': 'application/x-www-form-urlencoded'})
130192
logger.debug_secrets("session.cookies: %s", session.cookies)
131193
cookies = requests.utils.dict_from_cookiejar(session.cookies)
132194
logger.debug_secrets("cookies: %s", cookies)
@@ -139,16 +201,15 @@ def acquire_authentication_cookie(self, options):
139201
return False
140202

141203
@staticmethod
142-
def prepare_security_token_request(params):
204+
def _prepare_request_from_template(template_name, params):
143205
"""Construct the request body to acquire security token from STS endpoint"""
144-
logger = SamlTokenProvider.logger(SamlTokenProvider.prepare_security_token_request.__name__)
206+
logger = SamlTokenProvider.logger()
145207
logger.debug_secrets('params: %s', params)
146-
147-
f = open(os.path.join(os.path.dirname(__file__), 'SAML.xml'))
208+
f = open(os.path.join(os.path.dirname(__file__), template_name))
148209
try:
149210
data = f.read()
150211
for key in params:
151-
data = data.replace('[' + key + ']', params[key])
212+
data = data.replace('{' + key + '}', str(params[key]))
152213
return data
153214
finally:
154215
f.close()

office365/runtime/auth/sts_info.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import datetime
2+
3+
4+
class STSInfo(object):
5+
6+
def __init__(self, authority_url):
7+
self.authorityUrl = authority_url
8+
self.serviceUrl = 'https://login.microsoftonline.com'
9+
self.securityTokenServicePath = 'extSTS.srf'
10+
self.userRealmServicePath = 'GetUserRealm.srf'
11+
self.federationTokenIssuer = 'urn:federation:MicrosoftOnline'
12+
self.created = datetime.datetime.now()
13+
self.expires = self.created + datetime.timedelta(minutes=10)
14+
15+
@property
16+
def securityTokenServiceUrl(self):
17+
return "/".join([self.serviceUrl, self.securityTokenServicePath])
18+
19+
@property
20+
def signInPageUrl(self):
21+
return "/".join([self.authorityUrl, '_forms/default.aspx?wa=wsignin1.0'])
22+
23+
@property
24+
def userRealmServiceUrl(self):
25+
return '/'.join([self.serviceUrl, self.userRealmServicePath])

0 commit comments

Comments
 (0)