@@ -60,6 +60,41 @@ def find_service(id):
60
60
return service
61
61
62
62
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
+
63
98
class RedirectToLoginHandler (webapp2 .RequestHandler ):
64
99
"""Creates a state and redirects the user to the login page"""
65
100
@@ -129,12 +164,16 @@ def get(self):
129
164
if filtertype is None and n .has_key ('hidden' ) and n ['hidden' ]:
130
165
continue
131
166
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' )
135
174
136
- if tokenversion is not None :
137
- link += '&tokenversion=' + str (tokenversion )
175
+ if tokenversion is not None :
176
+ link += '&tokenversion=' + str (tokenversion )
138
177
139
178
notes = ''
140
179
if n .has_key ('notes' ):
@@ -309,39 +348,105 @@ def get(self, service=None):
309
348
logging .info ('Returned refresh token for service %s' , provider ['id' ])
310
349
return
311
350
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' ]
317
416
318
- exp_secs = 1800 # 30 min guess
319
417
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 )
321
421
except :
322
- pass
422
+ error = 'Error: Invalid CLI token'
423
+ raise
323
424
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
334
444
335
- entry = None
336
- keyid = None
445
+ resp = json .loads (content )
337
446
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 )
342
448
343
- # Return the id and password to the user
344
- authid = keyid + ':' + password
449
+ fetchtoken = dbmodel .create_fetch_token (resp )
345
450
346
451
# If this was part of a polling request, signal completion
347
452
dbmodel .update_fetch_token (fetchtoken , authid )
@@ -357,9 +462,8 @@ def get(self, service=None):
357
462
358
463
template = JINJA_ENVIRONMENT .get_template ('logged-in.html' )
359
464
self .response .write (template .render (template_values ))
360
- statetoken .delete ()
361
465
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 )
363
467
364
468
except :
365
469
logging .exception ('handler error for ' + display )
@@ -368,7 +472,7 @@ def get(self, service=None):
368
472
'service' : display ,
369
473
'appname' : settings .APP_NAME ,
370
474
'longappname' : settings .SERVICE_DISPLAYNAME ,
371
- 'authid' : 'Server error, close window and try again' ,
475
+ 'authid' : error ,
372
476
'fetchtoken' : ''
373
477
}
374
478
@@ -559,11 +663,14 @@ def process(self, authid):
559
663
url = service ['auth-url' ]
560
664
request_params = {
561
665
'client_id' : service ['client-id' ],
562
- 'redirect_uri' : service ['redirect-uri' ],
563
- 'client_secret' : service ['client-secret' ],
564
666
'grant_type' : 'refresh_token' ,
565
667
'refresh_token' : resp ['refresh_token' ]
566
668
}
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
+
567
674
# Some services do not allow the state to be passed
568
675
if service .has_key ('no-redirect_uri-for-refresh-request' ) and service ['no-redirect_uri-for-refresh-request' ]:
569
676
del request_params ['redirect_uri' ]
@@ -673,12 +780,17 @@ def handle_v2(self, inputfragment):
673
780
logging .info ('Cached response to: %s is invalid because it expires in %s' , tokenhash , exp_secs )
674
781
675
782
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 )
682
794
683
795
urlfetch .set_default_fetch_deadline (20 )
684
796
@@ -983,6 +1095,8 @@ def get(self):
983
1095
app = webapp2 .WSGIApplication ([
984
1096
('/logged-in' , LoginHandler ),
985
1097
('/login' , RedirectToLoginHandler ),
1098
+ ('/cli-token' , CliTokenHandler ),
1099
+ ('/cli-token-login' , CliTokenLoginHandler ),
986
1100
('/refresh' , RefreshHandler ),
987
1101
('/fetch' , FetchHandler ),
988
1102
('/token-state' , TokenStateHandler ),
0 commit comments