@@ -60,6 +60,41 @@ def find_service(id):
6060 return service
6161
6262
63+ def create_authtoken (provider_id , token ):
64+ # We store the ID if we get it back
65+ if token .has_key ("user_id" ):
66+ user_id = token ["user_id" ]
67+ else :
68+ user_id = "N/A"
69+
70+ exp_secs = 1800 # 30 min guess
71+ try :
72+ exp_secs = int (token ["expires_in" ])
73+ except :
74+ pass
75+
76+ # Create a random password and encrypt the response
77+ # This ensures that a hostile takeover will not get access
78+ # to stored access and refresh tokens
79+ password = password_generator .generate_pass ()
80+ cipher = simplecrypt .encrypt (password , json .dumps (token ))
81+
82+ # Convert to text and prepare for storage
83+ b64_cipher = base64 .b64encode (cipher )
84+ expires = datetime .datetime .utcnow () + datetime .timedelta (seconds = exp_secs )
85+
86+ entry = None
87+ keyid = None
88+
89+ # Find a random un-used user ID, and store the encrypted data
90+ while entry is None :
91+ keyid = '%030x' % random .randrange (16 ** 32 )
92+ entry = dbmodel .insert_new_authtoken (keyid , user_id , b64_cipher , expires , provider_id )
93+
94+ # Return the keyid and authid
95+ return keyid , keyid + ':' + password
96+
97+
6398class RedirectToLoginHandler (webapp2 .RequestHandler ):
6499 """Creates a state and redirects the user to the login page"""
65100
@@ -129,12 +164,16 @@ def get(self):
129164 if filtertype is None and n .has_key ('hidden' ) and n ['hidden' ]:
130165 continue
131166
132- link = '/login?id=' + n ['id' ]
133- if self .request .get ('token' , None ) is not None :
134- link += '&token=' + self .request .get ('token' )
167+ link = ''
168+ if service .has_key ('cli-token' ) and service ['cli-token' ]:
169+ link = '/cli-token?id=' + n ['id' ]
170+ else :
171+ link = '/login?id=' + n ['id' ]
172+ if self .request .get ('token' , None ) is not None :
173+ link += '&token=' + self .request .get ('token' )
135174
136- if tokenversion is not None :
137- link += '&tokenversion=' + str (tokenversion )
175+ if tokenversion is not None :
176+ link += '&tokenversion=' + str (tokenversion )
138177
139178 notes = ''
140179 if n .has_key ('notes' ):
@@ -309,39 +348,105 @@ def get(self, service=None):
309348 logging .info ('Returned refresh token for service %s' , provider ['id' ])
310349 return
311350
312- # We store the ID if we get it back
313- if resp .has_key ("user_id" ):
314- user_id = resp ["user_id" ]
315- else :
316- user_id = "N/A"
351+ # Return the id and password to the user
352+ keyid , authid = create_authtoken (provider ['id' ], resp )
353+
354+ fetchtoken = statetoken .fetchtoken
355+
356+ # If this was part of a polling request, signal completion
357+ dbmodel .update_fetch_token (fetchtoken , authid )
358+
359+ # Report results to the user
360+ template_values = {
361+ 'service' : display ,
362+ 'appname' : settings .APP_NAME ,
363+ 'longappname' : settings .SERVICE_DISPLAYNAME ,
364+ 'authid' : authid ,
365+ 'fetchtoken' : fetchtoken
366+ }
367+
368+ template = JINJA_ENVIRONMENT .get_template ('logged-in.html' )
369+ self .response .write (template .render (template_values ))
370+ statetoken .delete ()
371+
372+ logging .info ('Created new authid %s for service %s' , keyid , provider ['id' ])
373+
374+ except :
375+ logging .exception ('handler error for ' + display )
376+
377+ template_values = {
378+ 'service' : display ,
379+ 'appname' : settings .APP_NAME ,
380+ 'longappname' : settings .SERVICE_DISPLAYNAME ,
381+ 'authid' : 'Server error, close window and try again' ,
382+ 'fetchtoken' : ''
383+ }
384+
385+ template = JINJA_ENVIRONMENT .get_template ('logged-in.html' )
386+ self .response .write (template .render (template_values ))
387+
388+ class CliTokenHandler (webapp2 .RequestHandler ):
389+ """Renders the cli-token.html page"""
390+
391+ def get (self ):
392+
393+ provider , service = find_provider_and_service (self .request .get ('id' , None ))
394+
395+ template_values = {
396+ 'service' : provider ['display' ],
397+ 'appname' : settings .APP_NAME ,
398+ 'longappname' : settings .SERVICE_DISPLAYNAME ,
399+ 'id' : provider ['id' ]
400+ }
401+
402+ template = JINJA_ENVIRONMENT .get_template ('cli-token.html' )
403+ self .response .write (template .render (template_values ))
404+
405+
406+ class CliTokenLoginHandler (webapp2 .RequestHandler ):
407+ """Handler that processes cli-token login and redirects the user to the logged-in page"""
408+
409+ def post (self ):
410+ display = 'Unknown'
411+ error = 'Server error, close window and try again'
412+ try :
413+ id = self .request .POST .get ('id' )
414+ provider , service = find_provider_and_service (id )
415+ display = provider ['display' ]
317416
318- exp_secs = 1800 # 30 min guess
319417 try :
320- exp_secs = int (resp ["expires_in" ])
418+ data = self .request .POST .get ('token' )
419+ content = base64 .urlsafe_b64decode (str (data ) + '=' * (- len (data ) % 4 ))
420+ resp = json .loads (content )
321421 except :
322- pass
422+ error = 'Error: Invalid CLI token'
423+ raise
323424
324- # Create a random password and encrypt the response
325- # This ensures that a hostile takeover will not get access
326- # to stored access and refresh tokens
327- password = password_generator .generate_pass ()
328- cipher = simplecrypt .encrypt (password , json .dumps (resp ))
329-
330- # Convert to text and prepare for storage
331- b64_cipher = base64 .b64encode (cipher )
332- expires = datetime .datetime .utcnow () + datetime .timedelta (seconds = exp_secs )
333- fetchtoken = statetoken .fetchtoken
425+ urlfetch .set_default_fetch_deadline (20 )
426+ url = service ['auth-url' ]
427+ data = urllib .urlencode ({
428+ 'client_id' : service ['client-id' ],
429+ 'grant_type' : 'password' ,
430+ 'scope' : provider ['scope' ],
431+ 'username' : resp ['username' ],
432+ 'password' : resp ['auth_token' ]
433+ })
434+ try :
435+ req = urllib2 .Request (url , data , {'Content-Type' : 'application/x-www-form-urlencoded' })
436+ f = urllib2 .urlopen (req )
437+ content = f .read ()
438+ f .close ()
439+ except urllib2 .HTTPError as err :
440+ if err .code == 401 :
441+ # If trying to re-use a single-use cli token
442+ error = 'Error: CLI token could not be authorized, create a new and try again'
443+ raise err
334444
335- entry = None
336- keyid = None
445+ resp = json .loads (content )
337446
338- # Find a random un-used user ID, and store the encrypted data
339- while entry is None :
340- keyid = '%030x' % random .randrange (16 ** 32 )
341- entry = dbmodel .insert_new_authtoken (keyid , user_id , b64_cipher , expires , provider ['id' ])
447+ keyid , authid = create_authtoken (id , resp )
342448
343- # Return the id and password to the user
344- authid = keyid + ':' + password
449+ fetchtoken = dbmodel .create_fetch_token (resp )
345450
346451 # If this was part of a polling request, signal completion
347452 dbmodel .update_fetch_token (fetchtoken , authid )
@@ -357,9 +462,8 @@ def get(self, service=None):
357462
358463 template = JINJA_ENVIRONMENT .get_template ('logged-in.html' )
359464 self .response .write (template .render (template_values ))
360- statetoken .delete ()
361465
362- logging .info ('Created new authid %s for service %s' , keyid , provider [ 'id' ] )
466+ logging .info ('Created new authid %s for service %s' , keyid , id )
363467
364468 except :
365469 logging .exception ('handler error for ' + display )
@@ -368,7 +472,7 @@ def get(self, service=None):
368472 'service' : display ,
369473 'appname' : settings .APP_NAME ,
370474 'longappname' : settings .SERVICE_DISPLAYNAME ,
371- 'authid' : 'Server error, close window and try again' ,
475+ 'authid' : error ,
372476 'fetchtoken' : ''
373477 }
374478
@@ -559,11 +663,14 @@ def process(self, authid):
559663 url = service ['auth-url' ]
560664 request_params = {
561665 'client_id' : service ['client-id' ],
562- 'redirect_uri' : service ['redirect-uri' ],
563- 'client_secret' : service ['client-secret' ],
564666 'grant_type' : 'refresh_token' ,
565667 'refresh_token' : resp ['refresh_token' ]
566668 }
669+ if service .has_key ("client_secret" ):
670+ request_params ['client_secret' ] = service ['client-secret' ]
671+ if service .has_key ("redirect_uri" ):
672+ request_params ['redirect_uri' ] = service ['redirect-uri' ]
673+
567674 # Some services do not allow the state to be passed
568675 if service .has_key ('no-redirect_uri-for-refresh-request' ) and service ['no-redirect_uri-for-refresh-request' ]:
569676 del request_params ['redirect_uri' ]
@@ -673,12 +780,17 @@ def handle_v2(self, inputfragment):
673780 logging .info ('Cached response to: %s is invalid because it expires in %s' , tokenhash , exp_secs )
674781
675782 url = service ['auth-url' ]
676- data = urllib .urlencode ({'client_id' : service ['client-id' ],
677- 'redirect_uri' : service ['redirect-uri' ],
678- 'client_secret' : service ['client-secret' ],
679- 'grant_type' : 'refresh_token' ,
680- 'refresh_token' : refresh_token
681- })
783+ request_params = {
784+ 'client_id' : service ['client-id' ],
785+ 'grant_type' : 'refresh_token' ,
786+ 'refresh_token' : refresh_token
787+ }
788+ if service .has_key ("client_secret" ):
789+ request_params ['client_secret' ] = service ['client-secret' ]
790+ if service .has_key ("redirect_uri" ):
791+ request_params ['redirect_uri' ] = service ['redirect-uri' ]
792+
793+ data = urllib .urlencode (request_params )
682794
683795 urlfetch .set_default_fetch_deadline (20 )
684796
@@ -983,6 +1095,8 @@ def get(self):
9831095app = webapp2 .WSGIApplication ([
9841096 ('/logged-in' , LoginHandler ),
9851097 ('/login' , RedirectToLoginHandler ),
1098+ ('/cli-token' , CliTokenHandler ),
1099+ ('/cli-token-login' , CliTokenLoginHandler ),
9861100 ('/refresh' , RefreshHandler ),
9871101 ('/fetch' , FetchHandler ),
9881102 ('/token-state' , TokenStateHandler ),
0 commit comments