Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(100)

Side by Side Diff: remoting/client/appengine/auth.py

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

Powered by Google App Engine
This is Rietveld 408576698