11import os
2+ import uuid
23from xml .etree import ElementTree
3-
44import requests
55import requests .utils
6-
76import office365 .logger
87from 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
1011office365 .logger .ensure_debug_secrets ()
1112
1213
1314class 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\n response.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\n session.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\n session.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 ()
0 commit comments