Index: remoting/client/appengine/auth.py |
diff --git a/remoting/client/appengine/auth.py b/remoting/client/appengine/auth.py |
index 40433cc943cb06101e6a8730eaf275858c22bc01..54be3833e230bc5225984669fc449adbc3da6a17 100644 |
--- a/remoting/client/appengine/auth.py |
+++ b/remoting/client/appengine/auth.py |
@@ -12,59 +12,85 @@ initiating authentication flows, and for managing credential storage per user. |
""" |
import os |
+import re |
+import time |
+import urllib |
+from urlparse import urlparse |
-import gdata.gauth |
-import gdata.client |
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. |
+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() |
- 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 HasOAuth2Tokens(throws=True): |
+ oauth2_tokens = OAuth2Tokens.get_or_insert(GetUserId()) |
+ if oauth2_tokens.refresh_token: |
+ return True; |
+ return False; |
-def GetChromotingToken(throws=True): |
- """Retrieves the Chromoting OAuth token for the user. |
- Args: |
- throws: bool (optional) Default is True. Throws if no token. |
+def GetAccessToken(throws=True): |
+ oauth2_tokens = OAuth2Tokens.get_or_insert(GetUserId()) |
- 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: |
+ 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' |
+ } |
+ 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: |
+ 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): |
@@ -74,26 +100,22 @@ def GetXmppToken(throws=True): |
throws: bool (optional) Default is True. Throws if no token. |
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 +137,6 @@ 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 +156,102 @@ 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') |
+ parsed_url = urlparse(self.request.url) |
+ server = parsed_url.scheme + '://' + parsed_url.netloc |
+ form_fields = { |
+ 'client_id' : CLIENT_ID, |
+ 'client_secret' : CLIENT_SECRET, |
+ 'redirect_uri' : server + '/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) |