Skip to content

Commit ffeacc2

Browse files
committed
Added support for Jottacloud cli token
1 parent c7da181 commit ffeacc2

File tree

3 files changed

+208
-42
lines changed

3 files changed

+208
-42
lines changed

cli-token.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<title>{{longappname}}</title>
8+
9+
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
10+
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
11+
12+
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
13+
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
14+
15+
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
16+
<!--[if lt IE 9]>
17+
<script src="//oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
18+
<script src="//oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
19+
<![endif]-->
20+
</head>
21+
<body>
22+
<div class="jumbotron">
23+
<h1>{{appname}} for {{service}}</h1>
24+
25+
<p>Type in the CLI token</p>
26+
<form action="/cli-token-login" method="POST">
27+
<input type="hidden" id="id" name="id" value="{{id}}" />
28+
<input type="text" id="token" name="token" />
29+
<br/>
30+
<br/>
31+
<input class="btn btn-primary btn-lg" role="button" type="submit" value="Login" />
32+
</form>
33+
</div>
34+
</body>
35+
</html>

main.py

Lines changed: 156 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
6398
class 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):
9831095
app = 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),

settings.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@
160160
DROPBOX_AUTH_URL = 'https://api.dropboxapi.com/oauth2/token'
161161
DROPBOX_LOGIN_URL = 'https://www.dropbox.com/oauth2/authorize'
162162

163+
JOTTACLOUD_AUTH_URL = 'https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token'
164+
163165
LOOKUP = {
164166
'wl': {
165167
'display': 'Windows Live',
@@ -214,6 +216,7 @@
214216
'auth-url': BOX_AUTH_URL,
215217
'login-url': BOX_LOGIN_URL
216218
},
219+
217220
'dropbox': {
218221
'display': 'Dropbox',
219222
'client-id': DROPBOX_CLIENT_ID,
@@ -225,6 +228,13 @@
225228
'no-state-for-token-request': True,
226229
# Dropbox is a little picky
227230
'no-redirect_uri-for-refresh-request': True
231+
},
232+
233+
'jottacloud': {
234+
'display': 'Jottacloud',
235+
'client-id': "jottacli",
236+
'auth-url': JOTTACLOUD_AUTH_URL,
237+
'cli-token': True
228238
}
229239
}
230240

@@ -324,6 +334,13 @@
324334
'scope': 'files.content.write files.content.read files.metadata.read files.metadata.write',
325335
'extraurl': 'token_access_type=offline',
326336
'servicelink': 'https://dropbox.com'
337+
},
338+
{
339+
'display': 'Jottacloud',
340+
'type': 'jottacloud',
341+
'id': 'jottacloud',
342+
'scope': 'offline_access+openid',
343+
'servicelink': 'https://jottacloud.com'
327344
}
328345
]
329346

0 commit comments

Comments
 (0)