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

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: remove the hardcoded localhost 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 import re
16 import time
17 import urllib
18 from urlparse import urlparse
15 19
16 import gdata.gauth
17 import gdata.client
18 20
19 from google.appengine.ext import db 21 from google.appengine.ext import db
22 from google.appengine.api import urlfetch
20 from google.appengine.api import users 23 from google.appengine.api import users
21 from google.appengine.ext import webapp 24 from google.appengine.ext import webapp
22 from google.appengine.ext.webapp import template 25 from google.appengine.ext.webapp import template
23 from google.appengine.ext.webapp import util 26 from google.appengine.ext.webapp import util
24 from google.appengine.ext.webapp.util import login_required 27 from google.appengine.ext.webapp.util import login_required
25 28
29 from django.utils import simplejson as json
30
26 31
27 SCOPES = ['https://www.googleapis.com/auth/chromoting', 32 SCOPES = ['https://www.googleapis.com/auth/chromoting',
28 'https://www.googleapis.com/auth/googletalk' ] 33 'https://www.googleapis.com/auth/googletalk' ]
29 34
35 # Development OAuth2 ID and keys.
36 CLIENT_ID = ('440925447803-d9u05st5jjm3gbe865l0jeaujqfrufrn.'
37 'apps.googleusercontent.com')
38 CLIENT_SECRET = 'Nl4vSQEgDpPMP-1rDEsgs3V7'
39
30 40
31 class NotAuthenticated(Exception): 41 class NotAuthenticated(Exception):
32 """API requiring authentication is called with credentials.""" 42 """API requiring authentication is called with credentials."""
33 pass 43 pass
34 44
35 45
36 class OAuthInvalidSetup(Exception): 46 class XmppToken(db.Model):
37 """OAuth configuration on app is not complete.""" 47 auth_token = db.StringProperty()
38 pass
39 48
40 49
41 class OAuthConfig(db.Model): 50 class OAuth2Tokens(db.Model):
42 """Stores the configuration data for OAuth. 51 """Stores the Refresh and Access token information for OAuth2."""
43 52 refresh_token = db.StringProperty()
44 Currently used to store the consumer key and secret so that it does not need 53 access_token = db.StringProperty()
45 to be checked into the source tree. 54 access_token_expiration = db.IntegerProperty()
46 """
47 consumer_key = db.StringProperty()
48 consumer_secret = db.StringProperty()
49 httpxmppproxy = db.StringProperty()
50 55
51 56
52 def GetChromotingToken(throws=True): 57 def HasOAuth2Tokens(throws=True):
53 """Retrieves the Chromoting OAuth token for the user. 58 oauth2_tokens = OAuth2Tokens.get_or_insert(GetUserId())
59 if oauth2_tokens.refresh_token:
60 return True;
61 return False;
54 62
55 Args:
56 throws: bool (optional) Default is True. Throws if no token.
57 63
58 Returns: 64 def GetAccessToken(throws=True):
59 An gdata.gauth.OAuthHmacToken for the current user. 65 oauth2_tokens = OAuth2Tokens.get_or_insert(GetUserId())
60 """ 66
61 user = users.get_current_user() 67 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() 68 raise NotAuthenticated()
67 return access_token 69
70 if time.time() > oauth2_tokens.access_token_expiration:
71 form_fields = {
72 'client_id' : CLIENT_ID,
73 'client_secret' : CLIENT_SECRET,
74 'refresh_token' : oauth2_tokens.refresh_token,
75 'grant_type' : 'refresh_token'
76 }
77 form_data = urllib.urlencode(form_fields)
78 result = urlfetch.fetch(
79 url = 'https://accounts.google.com/o/oauth2/token',
80 payload = form_data,
81 method = urlfetch.POST,
82 headers = {'Content-Type': 'application/x-www-form-urlencoded'})
83 if result.status_code != 200:
84 raise 'something went wrong %d, %s <br />' % (
85 result.status_code, result.content)
86 oauth_json = json.loads(result.content)
87 oauth2_tokens.access_token = oauth_json['access_token']
88 # Give us 30 second buffer to hackily account for RTT on network request.
89 oauth2_tokens.access_token_expiration = (
90 int(oauth_json['expires_in'] + time.time() - 30))
91 oauth2_tokens.put()
92
93 return oauth2_tokens.access_token
68 94
69 95
70 def GetXmppToken(throws=True): 96 def GetXmppToken(throws=True):
71 """Retrieves the XMPP for Chromoting. 97 """Retrieves the XMPP for Chromoting.
72 98
73 Args: 99 Args:
74 throws: bool (optional) Default is True. Throws if no token. 100 throws: bool (optional) Default is True. Throws if no token.
75 101
76 Returns: 102 Returns:
77 An gdata.gauth.ClientLoginToken for the current user. 103 The auth token for the current user.
78 """ 104 """
79 user = users.get_current_user() 105 xmpp_token = XmppToken.get_or_insert(GetUserId())
80 access_token = None 106 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() 107 raise NotAuthenticated()
85 return access_token 108 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 109
93 110
94 def ClearXmppToken(): 111 def ClearXmppToken():
95 """Clears all Chromoting ClientLogin token state from the datastore.""" 112 """Clears all Chromoting ClientLogin token state from the datastore."""
96 DeleteToken('xmpp_token') 113 db.delete(db.Key.from_path('XmppToken', GetUserId()))
114
115
116 def ClearOAuth2Token():
117 """Clears all Chromoting ClientLogin token state from the datastore."""
118 db.delete(db.Key.from_path('OAuth2Tokens', GetUserId()))
97 119
98 120
99 def GetUserId(): 121 def GetUserId():
100 """Retrieves the user id for the current user. 122 """Retrieves the user id for the current user.
101 123
102 Returns: 124 Returns:
103 A string with the user id of the logged in user. 125 A string with the user id of the logged in user.
104 126
105 Raises: 127 Raises:
106 NotAuthenticated if the user is not logged in, or missing an id. 128 NotAuthenticated if the user is not logged in, or missing an id.
107 """ 129 """
108 user = users.get_current_user() 130 user = users.get_current_user()
109 if not user: 131 if not user:
110 raise NotAuthenticated() 132 raise NotAuthenticated()
111 133
112 if not user.user_id(): 134 if not user.user_id():
113 raise NotAuthenticated('no e-mail with google account!') 135 raise NotAuthenticated('no e-mail with google account!')
114 136
115 return user.user_id() 137 return user.user_id()
116 138
117 139
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():
158 """Generates a standard key path for this app's OAuth configuration."""
159 return db.Key.from_path('OAuthConfig', 'oauth_config')
160
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): 140 class XmppAuthHandler(webapp.RequestHandler):
229 """Prompts Google Accounts credentials and retrieves a ClientLogin token. 141 """Prompts Google Accounts credentials and retrieves a ClientLogin token.
230 142
231 This class takes the user's plaintext username and password, and then 143 This class takes the user's plaintext username and password, and then
232 posts a request to ClientLogin to get the access token. 144 posts a request to ClientLogin to get the access token.
233 145
234 THIS CLASS SHOULD NOT EXIST. 146 THIS CLASS SHOULD NOT EXIST.
235 147
236 We should NOT be taking a user's Google Accounts credentials in our webapp. 148 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 149 However, we need a ClientLogin token for jingle, and this is currently the
238 only known workaround. 150 only known workaround.
239 """ 151 """
240 @login_required 152 @login_required
241 def get(self): 153 def get(self):
242 ClearXmppToken() 154 ClearXmppToken()
243 path = os.path.join(os.path.dirname(__file__), 'client_login.html') 155 path = os.path.join(os.path.dirname(__file__), 'client_login.html')
244 self.response.out.write(template.render(path, {})) 156 self.response.out.write(template.render(path, {}))
245 157
246 def post(self): 158 def post(self):
247 client = gdata.client.GDClient()
248 email = self.request.get('username') 159 email = self.request.get('username')
249 password = self.request.get('password') 160 password = self.request.get('password')
250 try: 161 form_fields = {
251 client.ClientLogin( 162 'accountType' : 'HOSTED_OR_GOOGLE',
252 email, password, 'chromoclient', 'chromiumsync') 163 'Email' : self.request.get('username'),
253 SaveToken('xmpp_token', client.auth_token) 164 'Passwd' : self.request.get('password'),
254 except gdata.client.CaptchaChallenge: 165 'service' : 'chromiumsync',
255 self.response.out.write('You need to solve a Captcha. ' 166 'source' : 'chromoplex'
256 'Unforutnately, we still have to implement that.') 167 }
257 self.redirect('/') 168 form_data = urllib.urlencode(form_fields)
169 result = urlfetch.fetch(
170 url = 'https://www.google.com/accounts/ClientLogin',
171 payload = form_data,
172 method = urlfetch.POST,
173 headers = {'Content-Type': 'application/x-www-form-urlencoded'})
174 if result.status_code != 200:
175 self.response.out.write(result.content)
176 for i in result.headers:
177 self.response.headers[i] = result.headers[i]
178 self.response.set_status(result.status_code)
179 return
258 180
259 181 xmpp_token = XmppToken(key_name = GetUserId())
260 class ClearChromotingTokenHandler(webapp.RequestHandler): 182 xmpp_token.auth_token = re.search("Auth=(.*)", result.content).group(1)
261 """Endpoint for dropping the user's Chromoting token.""" 183 xmpp_token.put()
262 @login_required
263 def get(self):
264 ClearChromotingToken()
265 self.redirect('/') 184 self.redirect('/')
266 185
267 186
268 class ClearXmppTokenHandler(webapp.RequestHandler): 187 class ClearXmppTokenHandler(webapp.RequestHandler):
269 """Endpoint for dropping the user's Xmpp token.""" 188 """Endpoint for dropping the user's Xmpp token."""
270 @login_required 189 @login_required
271 def get(self): 190 def get(self):
272 ClearXmppToken() 191 ClearXmppToken()
273 self.redirect('/') 192 self.redirect('/')
274 193
275 194
276 class SetupOAuthHandler(webapp.RequestHandler): 195 class ClearOAuth2TokenHandler(webapp.RequestHandler):
277 """Administrative page for specifying the OAuth consumer key/secret.""" 196 """Endpoint for dropping the user's OAuth2 token."""
278 @login_required 197 @login_required
279 def get(self): 198 def get(self):
280 path = os.path.join(os.path.dirname(__file__), 199 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('/') 200 self.redirect('/')
312 201
313 def GetHttpXmppProxy(): 202
314 config = GetOAuthConfig(throws=True) 203 class OAuth2ReturnHandler(webapp.RequestHandler):
315 if not config.httpxmppproxy: 204 """Handles the redirect in the OAuth dance."""
316 raise OAuthInvalidSetup() 205 @login_required
317 return config.httpxmppproxy 206 def get(self):
207 code = self.request.get('code')
208 state = self.request.get('state')
209 parsed_url = urlparse(self.request.url)
210 server = parsed_url.scheme + '://' + parsed_url.netloc
211 form_fields = {
212 'client_id' : CLIENT_ID,
213 'client_secret' : CLIENT_SECRET,
214 'redirect_uri' : server + '/auth/oauth2_return',
215 'code' : code,
216 'grant_type' : 'authorization_code'
217 }
218 form_data = urllib.urlencode(form_fields)
219 result = urlfetch.fetch(
220 url = 'https://accounts.google.com/o/oauth2/token',
221 payload = form_data,
222 method = urlfetch.POST,
223 headers = {'Content-Type': 'application/x-www-form-urlencoded'})
224
225 if result.status_code != 200:
226 self.response.out.write('something went wrong %d, %s <br />' %
227 (result.status_code, result.content))
228 self.response.out.write(
229 'We tried posting %s code(%s) [%s]' % (form_data, code, form_fields))
230 self.response.set_status(400)
231 return
232
233 oauth_json = json.loads(result.content)
234 oauth2_tokens = OAuth2Tokens(key_name = GetUserId())
235 oauth2_tokens.refresh_token = oauth_json['refresh_token']
236 oauth2_tokens.access_token = oauth_json['access_token']
237 # Give us 30 second buffer to hackily account for RTT on network request.
238 oauth2_tokens.access_token_expiration = (
239 int(oauth_json['expires_in'] + time.time() - 30))
240 oauth2_tokens.put()
241
242 if state:
243 self.redirect(state)
244 else:
245 self.redirect('/')
246
318 247
319 def main(): 248 def main():
320 application = webapp.WSGIApplication( 249 application = webapp.WSGIApplication(
321 [ 250 [
322 ('/auth/chromoting_auth', ChromotingAuthHandler),
323 ('/auth/xmpp_auth', XmppAuthHandler), 251 ('/auth/xmpp_auth', XmppAuthHandler),
324 ('/auth/chromoting_auth_return', ChromotingAuthReturnHandler),
325 ('/auth/clear_xmpp_token', ClearXmppTokenHandler), 252 ('/auth/clear_xmpp_token', ClearXmppTokenHandler),
326 ('/auth/clear_chromoting_token', ClearChromotingTokenHandler), 253 ('/auth/clear_oauth2_token', ClearOAuth2TokenHandler),
327 ('/auth/setup_oauth', SetupOAuthHandler) 254 ('/auth/oauth2_return', OAuth2ReturnHandler)
328 ], 255 ],
329 debug=True) 256 debug=True)
330 util.run_wsgi_app(application) 257 util.run_wsgi_app(application)
331 258
332 259
333 if __name__ == '__main__': 260 if __name__ == '__main__':
334 main() 261 main()
OLDNEW
« no previous file with comments | « remoting/client/appengine/app.yaml ('k') | remoting/client/appengine/chromoting_oauth_setup.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698