1- """sample Microsoft Graph authentication library"""
1+ """Sample Microsoft Graph authentication library. """
22# Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license.
33# See LICENSE in the project root for license information.
44import json
1313
1414import config
1515
16- # disable warnings to allow use of non-HTTPS for local dev/test
16+
17+ # Disable warnings to allow use of non-HTTPS for local dev/test.
1718urllib3 .disable_warnings ()
1819
1920class GraphSession (object ):
20- """Microsoft Graph connection class. Implements OAuth 2.0 Authorization Code
21- Grant workflow, handles configuration and state management, adding tokens
22- for authenticated calls to Graph, related details.
21+ """Microsoft Graph connection class.
22+
23+ Implements OAuth 2.0 Authorization Code Grant workflow, handles
24+ configuration and state management, adding tokens for authenticated calls to
25+ Graph, related details.
2326 """
2427
2528 def __init__ (self , ** kwargs ):
@@ -60,7 +63,7 @@ def __init__(self, **kwargs):
6063 # Print warning if any unknown arguments were passed, since those may be
6164 # errors/typos.
6265 for key in kwargs :
63- if not key in self .config :
66+ if key not in self .config :
6467 print (f'WARNING: unknown "{ key } " argument passed to GraphSession' )
6568
6669 self .config .update (kwargs .items ()) # add passed arguments to config
@@ -80,8 +83,7 @@ def __init__(self, **kwargs):
8083 if self .config ['refresh_enable' ]:
8184 if refresh_scope not in self .config ['scopes' ]:
8285 self .config ['scopes' ].append (refresh_scope )
83- else :
84- if refresh_scope in self .config ['scopes' ]:
86+ elif refresh_scope in self .config ['scopes' ]:
8587 self .config ['scopes' ].remove (refresh_scope )
8688
8789 def __repr__ (self ):
@@ -92,14 +94,14 @@ def __repr__(self):
9294
9395 def api_endpoint (self , url ):
9496 """Convert relative endpoint (e.g., 'me') to full Graph API endpoint."""
95-
9697 if urllib .parse .urlparse (url ).scheme in ['http' , 'https' ]:
9798 return url
9899 return urllib .parse .urljoin (
99100 f"{ self .config ['resource' ]} { self .config ['api_version' ]} /" ,
100101 url .lstrip ('/' ))
101102
102- def delete (self , endpoint , * , headers = None , data = None , verify = False , params = None ):
103+ def delete (self , endpoint , * , headers = None , data = None , verify = False ,
104+ params = None ):
103105 """Wrapper for authenticated HTTP DELETE to API endpoint.
104106
105107 endpoint = URL (can be partial; for example, 'me/contacts')
@@ -132,10 +134,8 @@ def get(self, endpoint='me', *, headers=None, stream=False, verify=False, params
132134
133135 Returns Requests response object.
134136 """
135-
136137 self .token_validation ()
137-
138- # merge passed headers with default headers
138+ # Merge passed headers with default headers.
139139 merged_headers = self .headers ()
140140 if headers :
141141 merged_headers .update (headers )
@@ -145,7 +145,7 @@ def get(self, endpoint='me', *, headers=None, stream=False, verify=False, params
145145 stream = stream , verify = verify , params = params )
146146
147147 def headers (self , headers = None ):
148- """Returns dictionary of default HTTP headers for calls to Microsoft Graph API,
148+ """Return a dict of default HTTP headers for calls to Microsoft Graph API,
149149 including access token and a unique client-request-id.
150150
151151 Keyword arguments:
@@ -172,25 +172,29 @@ def login(self, login_redirect=None):
172172 """
173173 if login_redirect :
174174 self .login_redirect = login_redirect
175-
176- # if caching is enabled, attempt silent SSO first
175+ # If caching is enabled, attempt silent SSO first.
177176 if self .config ['cache_state' ]:
178177 if self .silent_sso ():
179178 return bottle .redirect (self .login_redirect )
180179
181180 self .authstate = str (uuid .uuid4 ())
182- params = urllib .parse .urlencode ({'response_type' : 'code' ,
183- 'client_id' : self .config ['client_id' ],
184- 'redirect_uri' : self .config ['redirect_uri' ],
185- 'scope' : ' ' .join (self .config ['scopes' ]),
186- 'state' : self .authstate ,
187- 'prompt' : 'select_account' })
188- self .state ['authorization_url' ] = self .config ['auth_endpoint' ] + '?' + params
181+ data = {
182+ 'response_type' : 'code' ,
183+ 'client_id' : self .config ['client_id' ],
184+ 'redirect_uri' : self .config ['redirect_uri' ],
185+ 'scope' : ' ' .join (self .config ['scopes' ]),
186+ 'state' : self .authstate ,
187+ 'prompt' : 'select_account' ,
188+ }
189+ params = urllib .parse .urlencode (data )
190+ url = f"{ self .config ['auth_endpoint' ]} ?{ params } "
191+ self .state ['authorization_url' ] = url
189192 bottle .redirect (self .state ['authorization_url' ], 302 )
190193
191194 def logout (self , redirect_to = None ):
192195 """Clear current Graph connection state and redirect to specified route.
193- If redirect_to == None, no redirection will take place and just clears
196+
197+ If redirect_to is false, no redirection will take place and just clears
194198 the current logged-in status.
195199 """
196200
@@ -226,9 +230,7 @@ def post(self, endpoint, headers=None, data=None, verify=False, params=None):
226230 to False for demo purposes. For more information see:
227231 http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification
228232 """
229-
230233 self .token_validation ()
231-
232234 merged_headers = self .headers ()
233235 if headers :
234236 merged_headers .update (headers )
@@ -261,20 +263,21 @@ def redirect_uri_handler(self):
261263 code received from auth endpoint to call the token endpoint and obtain
262264 an access token.
263265 """
264-
265266 # Verify that this authorization attempt came from this app, by checking
266267 # the received state against what we sent with our authorization request.
267268 if self .authstate != bottle .request .query .state :
268- raise Exception (f"STATE MISMATCH: { self .authstate } sent,"
269- f"{ bottle .request .query .state } received" )
269+ raise ValueError (f"STATE MISMATCH: { self .authstate } sent, "
270+ f"{ bottle .request .query .state } received" )
270271 self .authstate = '' # clear state to prevent re-use
271-
272+ data = {
273+ 'client_id' : self .config ['client_id' ],
274+ 'client_secret' : self .config ['client_secret' ],
275+ 'grant_type' : 'authorization_code' ,
276+ 'code' : bottle .request .query .code ,
277+ 'redirect_uri' : self .config ['redirect_uri' ]
278+ }
272279 token_response = requests .post (self .config ['token_endpoint' ],
273- data = {'client_id' : self .config ['client_id' ],
274- 'client_secret' : self .config ['client_secret' ],
275- 'grant_type' : 'authorization_code' ,
276- 'code' : bottle .request .query .code ,
277- 'redirect_uri' : self .config ['redirect_uri' ]})
280+ data = data )
278281 self .token_save (token_response )
279282
280283 if token_response and token_response .ok :
@@ -289,12 +292,10 @@ def silent_sso(self):
289292 """
290293 if self .token_seconds () > 0 :
291294 return True # current token is vald
292-
293- if self .state ['refresh_token' ]:
295+ elif self .state ['refresh_token' ]:
294296 # we have a refresh token, so use it to refresh the access token
295297 self .token_refresh ()
296298 return True
297-
298299 return False # can't do silent SSO at this time
299300
300301 def state_manager (self , action ):
@@ -304,7 +305,6 @@ def state_manager(self, action):
304305 'init' -- initialize state (set properties to defaults)
305306 'save' -- save current state (if self.config['cache_state'])
306307 """
307-
308308 initialized_state = {'access_token' : None , 'refresh_token' : None ,
309309 'token_expires_at' : 0 , 'authorization_url' : '' ,
310310 'token_scope' : '' , 'loggedin' : False }
@@ -325,12 +325,14 @@ def state_manager(self, action):
325325
326326 def token_refresh (self ):
327327 """Refresh the current access token."""
328+ data = {
329+ 'client_id' : self .config ['client_id' ],
330+ 'client_secret' : self .config ['client_secret' ],
331+ 'grant_type' : 'refresh_token' ,
332+ 'refresh_token' : self .state ['refresh_token' ],
333+ }
328334 response = requests .post (self .config ['token_endpoint' ],
329- data = {'client_id' : self .config ['client_id' ],
330- 'client_secret' : self .config ['client_secret' ],
331- 'grant_type' : 'refresh_token' ,
332- 'refresh_token' : self .state ['refresh_token' ]},
333- verify = False )
335+ data = data , verify = False )
334336 self .token_save (response )
335337
336338 def token_save (self , response ):
@@ -343,19 +345,18 @@ def token_save(self, response):
343345 Returns True if the token was successfully saved, False if not.
344346 To manually inspect the contents of a JWT, see http://jwt.ms/.
345347 """
346-
347- jsondata = response .json ()
348- if not 'access_token' in jsondata :
348+ json_data = response .json ()
349+ if 'access_token' not in json_data :
349350 self .logout ()
350351 return False
351352
352- self .verify_scopes (jsondata ['scope' ])
353- self .state ['access_token' ] = jsondata ['access_token' ]
353+ self .verify_scopes (json_data ['scope' ])
354+ self .state ['access_token' ] = json_data ['access_token' ]
354355 self .state ['loggedin' ] = True
355356
356357 # token_expires_at = time.time() value (seconds) at which it expires
357- self .state ['token_expires_at' ] = time .time () + int (jsondata ['expires_in' ])
358- self .state ['refresh_token' ] = jsondata .get ('refresh_token' , None )
358+ self .state ['token_expires_at' ] = time .time () + int (json_data ['expires_in' ])
359+ self .state ['refresh_token' ] = json_data .get ('refresh_token' )
359360 return True
360361
361362 def token_seconds (self ):
@@ -376,9 +377,7 @@ def token_validation(self, nseconds=5):
376377
377378 def verify_scopes (self , token_scopes ):
378379 """Verify that the list of scopes returned with an access token match
379- the scopes that we requested.
380- """
381-
380+ the scopes that we requested."""
382381 self .state ['token_scope' ] = token_scopes
383382 scopes_returned = frozenset ({_ .lower () for _ in token_scopes .split (' ' )})
384383 scopes_expected = frozenset ({_ .lower () for _ in self .config ['scopes' ]
0 commit comments