1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
|
#!/usr/bin/env python
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Provides authentcation related utilities and endpoint handlers.
All authentication code for the webapp should go through this module. In
general, credentials should be used server-side. The URL endpoints are for
initiating authentication flows, and for managing credential storage per user.
"""
import os
import gdata.gauth
import gdata.client
from google.appengine.ext import db
from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp import util
from google.appengine.ext.webapp.util import login_required
SCOPES = ['https://www.googleapis.com/auth/chromoting',
'https://www.googleapis.com/auth/googletalk' ]
class NotAuthenticated(Exception):
"""API requiring authentication is called with credentials."""
pass
class OAuthInvalidSetup(Exception):
"""OAuth configuration on app is not complete."""
pass
class OAuthConfig(db.Model):
"""Stores the configuration data for OAuth.
Currently used to store the consumer key and secret so that it does not need
to be checked into the source tree.
"""
consumer_key = db.StringProperty()
consumer_secret = db.StringProperty()
httpxmppproxy = db.StringProperty()
def GetChromotingToken(throws=True):
"""Retrieves the Chromoting OAuth token for the user.
Args:
throws: bool (optional) Default is True. Throws if no token.
Returns:
An gdata.gauth.OAuthHmacToken for the current user.
"""
user = users.get_current_user()
access_token = None
if user:
access_token = LoadToken('chromoting_token')
if throws and not access_token:
raise NotAuthenticated()
return access_token
def GetXmppToken(throws=True):
"""Retrieves the XMPP for Chromoting.
Args:
throws: bool (optional) Default is True. Throws if no token.
Returns:
An gdata.gauth.ClientLoginToken for the current user.
"""
user = users.get_current_user()
access_token = None
if user:
access_token = LoadToken('xmpp_token')
if throws and not access_token:
raise NotAuthenticated()
return access_token
def ClearChromotingToken():
"""Clears all Chromoting OAuth token state from the datastore."""
DeleteToken('request_token')
DeleteToken('chromoting_token')
def ClearXmppToken():
"""Clears all Chromoting ClientLogin token state from the datastore."""
DeleteToken('xmpp_token')
def GetUserId():
"""Retrieves the user id for the current user.
Returns:
A string with the user id of the logged in user.
Raises:
NotAuthenticated if the user is not logged in, or missing an id.
"""
user = users.get_current_user()
if not user:
raise NotAuthenticated()
if not user.user_id():
raise NotAuthenticated('no e-mail with google account!')
return user.user_id()
def LoadToken(name):
"""Leads a gdata auth token for the current user.
Tokens are scoped to each user, and retrieved by a name.
Args:
name: A string with the name of the token for the current user.
Returns:
The token associated with the name for the user.
"""
user_id = GetUserId();
return gdata.gauth.AeLoad(user_id + name)
def SaveToken(name, token):
"""Saves a gdata auth token for the current user.
Tokens are scoped to each user, and stored by a name.
Args:
name: A string with the name of the token.
"""
user_id = GetUserId();
gdata.gauth.AeSave(token, user_id + name)
def DeleteToken(name):
"""Deletes a stored gdata auth token for the current user.
Tokens are scoped to each user, and stored by a name.
Args:
name: A string with the name of the token.
"""
user_id = GetUserId();
gdata.gauth.AeDelete(user_id + name)
def OAuthConfigKey():
"""Generates a standard key path for this app's OAuth configuration."""
return db.Key.from_path('OAuthConfig', 'oauth_config')
def GetOAuthConfig(throws=True):
"""Retrieves the OAuthConfig for this app.
Returns:
The OAuthConfig object for this app.
Raises:
OAuthInvalidSetup if no OAuthConfig exists.
"""
config = db.get(OAuthConfigKey())
if throws and not config:
raise OAuthInvalidSetup()
return config
class ChromotingAuthHandler(webapp.RequestHandler):
"""Initiates getting the OAuth access token for the user.
This webapp uses 3-legged OAuth. This handlers performs the first step
of getting the OAuth request token, and then forwarding on to the
Google Accounts authorization endpoint for the second step. The final
step is completed by the ChromotingAuthReturnHandler below.
FYI, all three steps are collectively known as the "OAuth dance."
"""
@login_required
def get(self):
ClearChromotingToken()
client = gdata.client.GDClient()
oauth_callback_url = ('http://%s/auth/chromoting_auth_return' %
self.request.host)
request_token = client.GetOAuthToken(
SCOPES, oauth_callback_url, GetOAuthConfig().consumer_key,
consumer_secret=GetOAuthConfig().consumer_secret)
SaveToken('request_token', request_token)
domain = None # Not on an Google Apps domain.
auth_uri = request_token.generate_authorization_url()
self.redirect(str(auth_uri))
class ChromotingAuthReturnHandler(webapp.RequestHandler):
"""Finishes the authorization started in ChromotingAuthHandler.i
After the user authorizes the OAuth request token at the OAuth request
URL they were redirected to in ChromotingAuthHandler, OAuth will send
them back here with an auth token in the URL.
This handler retrievies the access token, and stores it completing the
OAuth dance.
"""
@login_required
def get(self):
saved_request_token = LoadToken('request_token')
DeleteToken('request_token')
request_token = gdata.gauth.AuthorizeRequestToken(
saved_request_token, self.request.uri)
# Upgrade the token and save in the user's datastore
client = gdata.client.GDClient()
access_token = client.GetAccessToken(request_token)
SaveToken('chromoting_token', access_token)
self.redirect("/")
class XmppAuthHandler(webapp.RequestHandler):
"""Prompts Google Accounts credentials and retrieves a ClientLogin token.
This class takes the user's plaintext username and password, and then
posts a request to ClientLogin to get the access token.
THIS CLASS SHOULD NOT EXIST.
We should NOT be taking a user's Google Accounts credentials in our webapp.
However, we need a ClientLogin token for jingle, and this is currently the
only known workaround.
"""
@login_required
def get(self):
ClearXmppToken()
path = os.path.join(os.path.dirname(__file__), 'client_login.html')
self.response.out.write(template.render(path, {}))
def post(self):
client = gdata.client.GDClient()
email = self.request.get('username')
password = self.request.get('password')
try:
client.ClientLogin(
email, password, 'chromoclient', 'chromiumsync')
SaveToken('xmpp_token', client.auth_token)
except gdata.client.CaptchaChallenge:
self.response.out.write('You need to solve a Captcha. '
'Unforutnately, we still have to implement that.')
self.redirect('/')
class ClearChromotingTokenHandler(webapp.RequestHandler):
"""Endpoint for dropping the user's Chromoting token."""
@login_required
def get(self):
ClearChromotingToken()
self.redirect('/')
class ClearXmppTokenHandler(webapp.RequestHandler):
"""Endpoint for dropping the user's Xmpp token."""
@login_required
def get(self):
ClearXmppToken()
self.redirect('/')
class SetupOAuthHandler(webapp.RequestHandler):
"""Administrative page for specifying the OAuth consumer key/secret."""
@login_required
def get(self):
path = os.path.join(os.path.dirname(__file__),
'chromoting_oauth_setup.html')
self.response.out.write(template.render(path, {}))
def post(self):
old_consumer_secret = self.request.get('old_consumer_secret')
query = OAuthConfig.all()
# If there is an existing key, only allow updating if you know the old
# key. This is a simple safeguard against random users hitting this page.
config = GetOAuthConfig(throws=False)
httpxmppproxy = self.request.get('httpxmppproxy')
# TODO(ajwong): THIS IS A TOTAL HACK! FIX WITH OWN PAGE.
# Currently, this form has one submit button, and 3 pieces of input:
# consumer_key, oauth_secret, and the httpxmppproxy address. The first
# two only get committed if the secret + key match. The third always
# get committed via this hack. Really, it should just be a separate
# field/page to set this up.
if httpxmppproxy:
config.httpxmppproxy = httpxmppproxy
config.put()
config.consumer_key = self.request.get('consumer_key')
if config:
if config.consumer_secret != old_consumer_secret:
self.response.set_status(400)
self.response.out.write('Incorrect old consumer secret')
return
else:
config = OAuthConfig(key_name = OAuthConfigKey().id_or_name())
config.consumer_key = self.request.get('consumer_key')
config.consumer_secret = self.request.get('new_consumer_secret')
config.put()
self.redirect('/')
def GetHttpXmppProxy():
config = GetOAuthConfig(throws=True)
if not config.httpxmppproxy:
raise OAuthInvalidSetup()
return config.httpxmppproxy
def main():
application = webapp.WSGIApplication(
[
('/auth/chromoting_auth', ChromotingAuthHandler),
('/auth/xmpp_auth', XmppAuthHandler),
('/auth/chromoting_auth_return', ChromotingAuthReturnHandler),
('/auth/clear_xmpp_token', ClearXmppTokenHandler),
('/auth/clear_chromoting_token', ClearChromotingTokenHandler),
('/auth/setup_oauth', SetupOAuthHandler)
],
debug=True)
util.run_wsgi_app(application)
if __name__ == '__main__':
main()
|