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

Unified 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 side-by-side diff with in-line comments
Download patch
Index: remoting/client/appengine/auth.py
diff --git a/remoting/client/appengine/auth.py b/remoting/client/appengine/auth.py
index 40433cc943cb06101e6a8730eaf275858c22bc01..c55ed5a87f53cb075620789cd6f7f99608ed9a71 100644
--- a/remoting/client/appengine/auth.py
+++ b/remoting/client/appengine/auth.py
@@ -12,59 +12,83 @@ initiating authentication flows, and for managing credential storage per user.
"""
import os
-
-import gdata.gauth
-import gdata.client
+import re
+import time
+import urllib
from google.appengine.ext import db
+from google.appengine.api import urlfetch
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
+from django.utils import simplejson as json
+
SCOPES = ['https://www.googleapis.com/auth/chromoting',
'https://www.googleapis.com/auth/googletalk' ]
+# Development OAuth2 ID and keys.
+CLIENT_ID = ('440925447803-d9u05st5jjm3gbe865l0jeaujqfrufrn.'
+ 'apps.googleusercontent.com')
+CLIENT_SECRET = 'Nl4vSQEgDpPMP-1rDEsgs3V7'
+
class NotAuthenticated(Exception):
"""API requiring authentication is called with credentials."""
pass
-class OAuthInvalidSetup(Exception):
- """OAuth configuration on app is not complete."""
- pass
+class XmppToken(db.Model):
+ auth_token = db.StringProperty()
-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()
+class OAuth2Tokens(db.Model):
+ """Stores the Refresh and Access token information for OAuth2."""
+ refresh_token = db.StringProperty()
+ access_token = db.StringProperty()
+ access_token_expiration = db.IntegerProperty()
-def GetChromotingToken(throws=True):
- """Retrieves the Chromoting OAuth token for the user.
+def HasOAuth2Tokens(throws=True):
+ oauth2_tokens = OAuth2Tokens.get_or_insert(GetUserId())
+ if oauth2_tokens.refresh_token:
+ return True;
+ return False;
- 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:
+def GetAccessToken(throws=True):
+ oauth2_tokens = OAuth2Tokens.get_or_insert(GetUserId())
+
+ if not oauth2_tokens.refresh_token:
raise NotAuthenticated()
- return access_token
+
+ if time.time() > oauth2_tokens.access_token_expiration:
+ form_fields = {
+ 'client_id' : CLIENT_ID,
+ 'client_secret' : CLIENT_SECRET,
+ 'refresh_token' : oauth2_tokens.refresh_token,
+ 'grant_type' : 'refresh_token'
+ }
+ result = urlfetch.fetch(
+ url = 'https://accounts.google.com/o/oauth2/token',
+ payload = form_data,
+ method = urlfetch.POST,
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'})
+ if result.status_code != 200:
+ raise 'something went wrong %d, %s <br />' % (
+ result.status_code, result.content)
+ oauth_json = json.loads(result.content)
+ oauth2_tokens.access_token = oauth_json['access_token']
+ # Give us 30 second buffer to hackily account for RTT on network request.
+ oauth2_tokens.access_token_expiration = (
+ int(oauth_json['expires_in'] + time.time() - 30))
+ oauth2_tokens.put()
+
+ return oauth2_tokens.access_token
def GetXmppToken(throws=True):
@@ -73,27 +97,24 @@ def GetXmppToken(throws=True):
Args:
throws: bool (optional) Default is True. Throws if no token.
+
Jamie 2011/05/20 16:46:47 Nit: No need for this blank line.
awong 2011/05/20 20:37:38 Done.
Returns:
- An gdata.gauth.ClientLoginToken for the current user.
+ The auth token 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:
+ xmpp_token = XmppToken.get_or_insert(GetUserId())
+ if throws and not xmpp_token.auth_token:
raise NotAuthenticated()
- return access_token
+ return xmpp_token.auth_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."""
+ db.delete(db.Key.from_path('XmppToken', GetUserId()))
-def ClearXmppToken():
+def ClearOAuth2Token():
"""Clears all Chromoting ClientLogin token state from the datastore."""
- DeleteToken('xmpp_token')
+ db.delete(db.Key.from_path('OAuth2Tokens', GetUserId()))
def GetUserId():
@@ -115,116 +136,10 @@ def GetUserId():
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.
@@ -244,87 +159,100 @@ class XmppAuthHandler(webapp.RequestHandler):
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.')
+ form_fields = {
+ 'accountType' : 'HOSTED_OR_GOOGLE',
+ 'Email' : self.request.get('username'),
+ 'Passwd' : self.request.get('password'),
+ 'service' : 'chromiumsync',
+ 'source' : 'chromoplex'
+ }
+ form_data = urllib.urlencode(form_fields)
+ result = urlfetch.fetch(
+ url = 'https://www.google.com/accounts/ClientLogin',
+ payload = form_data,
+ method = urlfetch.POST,
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'})
+ if result.status_code != 200:
+ self.response.out.write(result.content)
+ for i in result.headers:
+ self.response.headers[i] = result.headers[i]
+ self.response.set_status(result.status_code)
+ return
+
+ xmpp_token = XmppToken(key_name = GetUserId())
+ xmpp_token.auth_token = re.search("Auth=(.*)", result.content).group(1)
+ xmpp_token.put()
self.redirect('/')
-class ClearChromotingTokenHandler(webapp.RequestHandler):
- """Endpoint for dropping the user's Chromoting token."""
+class ClearXmppTokenHandler(webapp.RequestHandler):
+ """Endpoint for dropping the user's Xmpp token."""
@login_required
def get(self):
- ClearChromotingToken()
+ ClearXmppToken()
self.redirect('/')
-class ClearXmppTokenHandler(webapp.RequestHandler):
- """Endpoint for dropping the user's Xmpp token."""
+class ClearOAuth2TokenHandler(webapp.RequestHandler):
+ """Endpoint for dropping the user's OAuth2 token."""
@login_required
def get(self):
- ClearXmppToken()
+ ClearOAuth2Token()
self.redirect('/')
-class SetupOAuthHandler(webapp.RequestHandler):
- """Administrative page for specifying the OAuth consumer key/secret."""
+class OAuth2ReturnHandler(webapp.RequestHandler):
+ """Handles the redirect in the OAuth dance."""
@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)
- if config:
- if config.consumer_secret != old_consumer_secret:
- self.response.set_status(400)
- self.response.out.write('Incorrect old consumer secret')
- return
+ code = self.request.get('code')
+ state = self.request.get('state')
+ form_fields = {
+ 'client_id' : CLIENT_ID,
+ 'client_secret' : CLIENT_SECRET,
+ 'redirect_uri' : 'http://localhost:8080/auth/oauth2_return',
+ 'code' : code,
+ 'grant_type' : 'authorization_code'
+ }
+ form_data = urllib.urlencode(form_fields)
+ result = urlfetch.fetch(
+ url = 'https://accounts.google.com/o/oauth2/token',
+ payload = form_data,
+ method = urlfetch.POST,
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'})
+
+ if result.status_code != 200:
+ self.response.out.write('something went wrong %d, %s <br />' %
+ (result.status_code, result.content))
+ self.response.out.write(
+ 'We tried posting %s code(%s) [%s]' % (form_data, code, form_fields))
+ self.response.set_status(400)
+ return
+
+ oauth_json = json.loads(result.content)
+ oauth2_tokens = OAuth2Tokens(key_name = GetUserId())
+ oauth2_tokens.refresh_token = oauth_json['refresh_token']
+ oauth2_tokens.access_token = oauth_json['access_token']
+ # Give us 30 second buffer to hackily account for RTT on network request.
+ oauth2_tokens.access_token_expiration = (
+ int(oauth_json['expires_in'] + time.time() - 30))
+ oauth2_tokens.put()
+
+ if state:
+ self.redirect(state)
else:
- config = OAuthConfig(key_name = OAuthConfigKey().id_or_name())
-
- # 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
- # HTTP/XMPP proxy should really have its own configuration page.
- httpxmppproxy = self.request.get('httpxmppproxy')
- if httpxmppproxy:
- config.httpxmppproxy = httpxmppproxy
-
- config.consumer_key = self.request.get('consumer_key')
- config.consumer_secret = self.request.get('new_consumer_secret')
- config.put()
- self.redirect('/')
+ 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)
+ ('/auth/clear_oauth2_token', ClearOAuth2TokenHandler),
+ ('/auth/oauth2_return', OAuth2ReturnHandler)
],
debug=True)
util.run_wsgi_app(application)

Powered by Google App Engine
This is Rietveld 408576698