add openid+oauth support, google almost working (oauth not returning due to dns issue)

This commit is contained in:
mixedpuppy 2010-09-02 18:29:01 -07:00
Родитель 7fb9138354
Коммит ed3d6b26bf
10 изменённых файлов: 499 добавлений и 18 удалений

Просмотреть файл

@ -18,6 +18,9 @@ oauth.twitter.com.consumer_secret = prh6A961516mJ3XEjd7eERsGxuVZqycrBB6lV7LQ
oauth.facebook.com.app_id = 158102624846
oauth.facebook.com.app_secret = 4203f7f23803f405e06509ec4d4b9729
oauth.google.com.consumer_key = anonymous
oauth.google.com.consumer_secret = anonymous
[composite:main]
use = egg:Paste#urlmap

Просмотреть файл

@ -74,17 +74,16 @@ the contacts API that uses @me/@self.
service = get_provider(provider)
auth = service.responder()
access_key = auth.verify()
data = auth.get_credentials(access_key)
#import sys; print >> sys.stderr, data
user = auth.verify()
import sys; print >> sys.stderr, user
account = data['profile']['accounts'][0]
account = user['profile']['accounts'][0]
acct = self._get_or_create_account(provider, account['userid'], account['username'])
acct.profile = data['profile']
acct.oauth_token = data['oauth_token']
if 'oauth_token_secret' in data:
acct.oauth_token_secret = data['oauth_token_secret']
acct.profile = user['profile']
acct.oauth_token = user.get('oauth_token', None)
if 'oauth_token_secret' in user:
acct.oauth_token_secret = user['oauth_token_secret']
Session.commit()
fragment = "oauth_success_" + provider

Просмотреть файл

@ -1,5 +1,5 @@
from linkdrop.lib.oauth import facebook_
#from linkdrop.lib.oauth.google_ import GoogleResponder
from linkdrop.lib.oauth import google_
#from linkdrop.lib.oauth.live_ import LiveResponder
#from linkdrop.lib.oauth.openidconsumer import OpenIDResponder
from linkdrop.lib.oauth import twitter_
@ -10,7 +10,8 @@ __all__ = ['get_provider']
# XXX need a better way to do this
_providers = {
twitter_.domain: twitter_,
facebook_.domain: facebook_
facebook_.domain: facebook_,
google_.domain: google_
}
def get_provider(provider):

Просмотреть файл

@ -78,9 +78,9 @@ class OAuth1():
redirect(session['end_point_auth_failure'])
access_token = dict(urlparse.parse_qsl(content))
return access_token
return self._get_credentials(access_token)
def get_credentials(self, access_token):
def _get_credentials(self, access_token):
return access_token
class OAuth2():
@ -109,7 +109,7 @@ class OAuth2():
def verify(self):
code = request.GET.get('code')
if not code:
redirect(session['end_point_auth_failure'])
raise Exception("No oauth code received")
return_to = url(controller='account', action="verify",
qualified=True)
@ -124,7 +124,7 @@ class OAuth2():
raise Exception("Error status: %r", resp['status'])
access_token = parse_qs(content)['access_token'][0]
return access_token
return self._get_credentials(access_token)
def get_credentials(self, access_token):
def _get_credentials(self, access_token):
return access_token

Просмотреть файл

@ -97,7 +97,7 @@ class responder(OAuth2):
self.authorization_url = 'https://graph.facebook.com/oauth/authorize'
self.access_token_url = 'https://graph.facebook.com/oauth/access_token'
def get_credentials(self, access_token):
def _get_credentials(self, access_token):
fields = 'id,first_name,last_name,name,link,birthday,email,website,verified,picture,gender,timezone'
client = httplib2.Http()
resp, content = client.request(url(self.profile_url, access_token=access_token, fields=fields))

Просмотреть файл

@ -0,0 +1,96 @@
"""Google Responder
A Google responder that authenticates against Google using OpenID, or optionally
can use OpenId+OAuth hybrid protocol to request access to Google Apps using OAuth2.
"""
import urlparse
from openid.extensions import ax
import oauth2 as oauth
from pylons import config, request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
from linkdrop.lib.oauth.oid_extensions import OAuthRequest
from linkdrop.lib.oauth.oid_extensions import UIRequest
from linkdrop.lib.oauth.openidconsumer import ax_attributes, alternate_ax_attributes, attributes
from linkdrop.lib.oauth.openidconsumer import OpenIDResponder
GOOGLE_OAUTH = 'https://www.google.com/accounts/OAuthGetAccessToken'
domain = 'google.com'
class responder(OpenIDResponder):
def __init__(self, consumer=None, oauth_key=None, oauth_secret=None, request_attributes=None, *args,
**kwargs):
"""Handle Google Auth
This also handles making an OAuth request during the OpenID
authentication.
"""
OpenIDResponder.__init__(self, domain)
self.consumer_key = self.config.get('consumer_key')
self.consumer_secret = self.config.get('consumer_secret')
def _lookup_identifier(self, identifier):
"""Return the Google OpenID directed endpoint"""
return "https://www.google.com/accounts/o8/id"
def _update_authrequest(self, authrequest):
"""Update the authrequest with Attribute Exchange and optionally OAuth
To optionally request OAuth, the request POST must include an ``oauth_scope``
parameter that indicates what Google Apps should have access requested.
"""
request_attributes = request.POST.get('ax_attributes',
['country', 'email', 'first_name', 'last_name', 'language'])
ax_request = ax.FetchRequest()
for attr in request_attributes:
ax_request.add(ax.AttrInfo(attributes[attr], required=True))
authrequest.addExtension(ax_request)
oauth_request = OAuthRequest(consumer=self.consumer_key, scope=request.POST.get('scope', 'http://www.google.com/m8/feeds/'))
authrequest.addExtension(oauth_request)
if 'popup_mode' in request.POST:
kw_args = {'mode': request.POST['popup_mode']}
if 'popup_icon' in request.POST:
kw_args['icon'] = request.POST['popup_icon']
ui_request = UIRequest(**kw_args)
authrequest.addExtension(ui_request)
return None
def _get_access_token(self, request_token):
"""Retrieve the access token if OAuth hybrid was used"""
consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
token = oauth.Token(key=request_token, secret='')
client = oauth.Client(consumer, token)
resp, content = client.request(GOOGLE_OAUTH, "POST")
if resp['status'] != '200':
return None
return dict(urlparse.parse_qsl(content))
def _get_credentials(self, result_data):
#{'profile': {'preferredUsername': u'mixedpuppy',
# 'displayName': u'Shane Caraveo',
# 'name':
# {'givenName': u'Shane',
# 'formatted': u'Shane Caraveo',
# 'familyName': u'Caraveo'},
# 'providerName': 'Google',
# 'verifiedEmail': u'mixedpuppy@gmail.com',
# 'identifier': 'https://www.google.com/accounts/o8/id?id=AItOawnEHbJcEY5EtwX7vf81_x2P4KUjha35VyQ'}}
profile = result_data['profile']
userid = profile['verifiedEmail']
username = profile['preferredUsername']
profile['emails'] = [{ 'value': userid, 'primary': True }]
account = {'domain': domain,
'userid': userid,
'username': username }
profile['accounts'] = [account]
return result_data

Просмотреть файл

@ -0,0 +1,38 @@
"""OpenID Extensions
Additional OpenID extensions for OAuth and UIRequest extensions.
original code from velruse
"""
from openid import extension
class UIRequest(extension.Extension):
"""OpenID UI extension"""
ns_uri = 'http://specs.openid.net/extensions/ui/1.0'
ns_alias = 'ui'
def __init__(self, mode=None, icon=False):
super(UIRequest, self).__init__()
self._args = {}
if mode:
self._args['mode'] = mode
if icon:
self._args['icon'] = str(icon).lower()
def getExtensionArgs(self):
return self._args
class OAuthRequest(extension.Extension):
"""OAuth extension"""
ns_uri = 'http://specs.openid.net/extensions/oauth/1.0'
ns_alias = 'oauth'
def __init__(self, consumer, scope=None):
super(OAuthRequest, self).__init__()
self._args = {'consumer': consumer}
if scope:
self._args['scope'] = scope
def getExtensionArgs(self):
return self._args

Просмотреть файл

@ -0,0 +1,319 @@
import logging
import re
from openid.consumer import consumer
from openid.extensions import ax, sreg, pape
from openid.store import memstore, filestore, sqlstore
from pylons import config, request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
from linkdrop.lib.oauth.base import get_oauth_config
log = logging.getLogger(__name__)
__all__ = ['OpenIDResponder']
# Setup our attribute objects that we'll be requesting
ax_attributes = dict(
nickname = 'http://axschema.org/namePerson/friendly',
email = 'http://axschema.org/contact/email',
full_name = 'http://axschema.org/namePerson',
birthday = 'http://axschema.org/birthDate',
gender = 'http://axschema.org/person/gender',
postal_code = 'http://axschema.org/contact/postalCode/home',
country = 'http://axschema.org/contact/country/home',
timezone = 'http://axschema.org/pref/timezone',
language = 'http://axschema.org/pref/language',
name_prefix = 'http://axschema.org/namePerson/prefix',
first_name = 'http://axschema.org/namePerson/first',
last_name = 'http://axschema.org/namePerson/last',
middle_name = 'http://axschema.org/namePerson/middle',
name_suffix = 'http://axschema.org/namePerson/suffix',
web = 'http://axschema.org/contact/web/default',
)
#Change names later to make things a little bit clearer
alternate_ax_attributes = dict(
nickname = 'http://schema.openid.net/namePerson/friendly',
email = 'http://schema.openid.net/contact/email',
full_name = 'http://schema.openid.net/namePerson',
birthday = 'http://schema.openid.net/birthDate',
gender = 'http://schema.openid.net/person/gender',
postal_code = 'http://schema.openid.net/contact/postalCode/home',
country = 'http://schema.openid.net/contact/country/home',
timezone = 'http://schema.openid.net/pref/timezone',
language = 'http://schema.openid.net/pref/language',
name_prefix = 'http://schema.openid.net/namePerson/prefix',
first_name = 'http://schema.openid.net/namePerson/first',
last_name = 'http://schema.openid.net/namePerson/last',
middle_name = 'http://schema.openid.net/namePerson/middle',
name_suffix = 'http://schema.openid.net/namePerson/suffix',
web = 'http://schema.openid.net/contact/web/default',
)
# Translation dict for AX attrib names to sreg equiv
trans_dict = dict(
full_name = 'fullname',
birthday = 'dob',
postal_code = 'postcode',
)
attributes = ax_attributes
class AttribAccess(object):
"""Uniform attribute accessor for Simple Reg and Attribute Exchange values"""
def __init__(self, sreg_resp, ax_resp):
self.sreg_resp = sreg_resp or {}
self.ax_resp = ax_resp or ax.AXKeyValueMessage()
def get(self, key, ax_only=False):
"""Get a value from either Simple Reg or AX"""
# First attempt to fetch it from AX
v = self.ax_resp.getSingle(attributes[key])
if v:
return v
if ax_only:
return None
# Translate the key if needed
if key in trans_dict:
key = trans_dict[key]
# Don't attempt to fetch keys that aren't valid sreg fields
if key not in sreg.data_fields:
return None
return self.sreg_resp.get(key)
def extract_openid_data(identifier, sreg_resp, ax_resp):
"""Extract the OpenID Data from Simple Reg and AX data
This normalizes the data to the appropriate format.
"""
attribs = AttribAccess(sreg_resp, ax_resp)
ud = {'identifier': identifier}
if 'google.com' in identifier:
ud['providerName'] = 'Google'
elif 'yahoo.com' in identifier:
ud['providerName'] = 'Yahoo'
else:
ud['providerName'] = 'OpenID'
# Sort out the display name and preferred username
if ud['providerName'] == 'Google':
# Extract the first bit as the username since Google doesn't return
# any usable nickname info
email = attribs.get('email')
if email:
ud['preferredUsername'] = re.match('(^.*?)@', email).groups()[0]
else:
ud['preferredUsername'] = attribs.get('nickname')
# We trust that Google and Yahoo both verify their email addresses
if ud['providerName'] in ['Google', 'Yahoo']:
ud['verifiedEmail'] = attribs.get('email', ax_only=True)
else:
ud['emails'] = [attribs.get('email')]
# Parse through the name parts, assign the properly if present
name = {}
name_keys = ['name_prefix', 'first_name', 'middle_name', 'last_name', 'name_suffix']
pcard_map = {'first_name': 'givenName', 'middle_name': 'middleName', 'last_name': 'familyName',
'name_prefix': 'honorificPrefix', 'name_suffix': 'honorificSuffix'}
full_name_vals = []
for part in name_keys:
val = attribs.get(part)
if val:
full_name_vals.append(val)
name[pcard_map[part]] = val
full_name = ' '.join(full_name_vals).strip()
if not full_name:
full_name = attribs.get('full_name')
name['formatted'] = full_name
ud['name'] = name
ud['displayName'] = full_name or ud.get('preferredUsername')
urls = attribs.get('web')
if urls:
ud['urls'] = [urls]
for k in ['gender', 'birthday']:
ud[k] = attribs.get(k)
# Now strip out empty values
for k, v in ud.items():
if not v or (isinstance(v, list) and not v[0]):
del ud[k]
return ud
class OpenIDResponder():
"""OpenID Consumer for handling OpenID authentication
"""
def __init__(self, provider):
self.log_debug = logging.DEBUG >= log.getEffectiveLevel()
self.config = get_oauth_config(provider)
self.endpoint_regex = self.config.get('endpoint_regex')
# application config items, dont use self.config
store = config.get('openid_store', 'mem')
if store==u"file":
store_file_path = config.get('openid_store_path', None)
self.openid_store = filestore.FileOpenIDStore(store_file_path)
elif store==u"mem":
self.openid_store = memstore.MemoryStore()
elif store==u"sql":
# TODO: This does not work as we need a connection, not a string
self.openid_store = sqlstore.SQLStore(sql_connstring, sql_associations_table, sql_connstring)
def _lookup_identifier(self, identifier):
"""Extension point for inherited classes that want to change or set
a default identifier"""
return identifier
def _update_authrequest(self, authrequest):
"""Update the authrequest with the default extensions and attributes
we ask for
This method doesn't need to return anything, since the extensions
should be added to the authrequest object itself.
"""
# Add on the Attribute Exchange for those that support that
ax_request = ax.FetchRequest()
for attrib in attributes.values():
ax_request.add(ax.AttrInfo(attrib))
authrequest.addExtension(ax_request)
# Form the Simple Reg request
sreg_request = sreg.SRegRequest(
optional=['nickname', 'email', 'fullname', 'dob', 'gender', 'postcode',
'country', 'language', 'timezone'],
)
authrequest.addExtension(sreg_request)
return None
def _get_access_token(self, request_token):
"""Called to exchange a request token for the access token
This method doesn't by default return anything, other OpenID+Oauth
consumers should override it to do the appropriate lookup for the
access token, and return the access token.
"""
return None
def request_access(self):
log_debug = self.log_debug
if log_debug:
log.debug('Handling OpenID login')
# Load default parameters that all Auth Responders take
session['end_point_success'] = request.POST['end_point_success']
session['end_point_auth_failure'] = request.POST['end_point_auth_failure']
openid_url = request.POST.get('openid_identifier')
# Let inherited consumers alter the openid identifier if desired
openid_url = self._lookup_identifier(openid_url)
if not openid_url or (self.endpoint_regex and not re.match(self.endpoint_regex, end_point)):
return redirect(session['end_point_auth_failure'])
openid_session = {}
oidconsumer = consumer.Consumer(openid_session, self.openid_store)
try:
authrequest = oidconsumer.begin(openid_url)
except consumer.DiscoveryFailure:
return redirect(session['end_point_auth_failure'])
if authrequest is None:
return redirect(session['end_point_auth_failure'])
# Update the authrequest
self._update_authrequest(authrequest)
return_to = url(controller='account', action="verify",
qualified=True)
# Ensure our session is saved for the id to persist
session['openid_session'] = openid_session
session.save()
redirect_url = authrequest.redirectURL(realm=request.application_url,
return_to=return_to,
immediate=False)
return redirect(redirect_url)
# OpenID 2.0 lets Providers request POST instead of redirect, this
# checks for such a request.
if authrequest.shouldSendRedirect():
redirect_url = authrequest.redirectURL(realm=request.application_url,
return_to=return_to,
immediate=False)
return redirect(redirect_url)
else:
# XXX this will likely fail for now
html = authrequest.htmlMarkup(realm=request.application_url, return_to=return_to,
immediate=False)
return response(body=html)
def verify(self):
"""Handle incoming redirect from OpenID Provider"""
log_debug = self.log_debug
if log_debug:
log.debug('Handling processing of response from server')
openid_session = session['openid_session']
if not openid_session:
raise Exception("openid session missing")
# Setup the consumer and parse the information coming back
oidconsumer = consumer.Consumer(openid_session, self.openid_store)
return_to = url(controller='account', action="verify",
qualified=True)
info = oidconsumer.complete(request.params, return_to)
if info.status == consumer.FAILURE:
raise Exception("consumer failure")
elif info.status == consumer.CANCEL:
raise Exception("consumer canceled")
elif info.status == consumer.SUCCESS:
openid_identity = info.identity_url
if info.endpoint.canonicalID:
# If it's an i-name, use the canonicalID as its secure even if
# the old one is compromised
openid_identity = info.endpoint.canonicalID
user_data = extract_openid_data(identifier=openid_identity,
sreg_resp=sreg.SRegResponse.fromSuccessResponse(info),
ax_resp=ax.FetchResponse.fromSuccessResponse(info))
result_data = {'profile': user_data}
# Did we get any OAuth info?
access_token = None
oauth = info.extensionResponse('http://specs.openid.net/extensions/oauth/1.0', False)
if oauth and 'request_token' in oauth:
access_token = self._get_access_token(oauth['request_token'])
if access_token:
result_data['oauth_token'] = access_token
return self._get_credentials(result_data)
else:
raise Exception("unknown openid failure")
def _get_credentials(self, access_token):
return access_token

Просмотреть файл

@ -21,7 +21,7 @@ class responder(OAuth1):
self.access_token_url = 'https://twitter.com/oauth/access_token'
self.authorization_url = 'https://twitter.com/oauth/authenticate'
def get_credentials(self, access_token):
def _get_credentials(self, access_token):
# XXX should call twitter.api.VerifyCredentials to get the user object
# Setup the normalized poco contact object
username = access_token['screen_name']

Просмотреть файл

@ -156,6 +156,31 @@
</div>
</div>
<!-- Google section -->
<div class="section google hidden">
<form id="oauthForm" action="/api/account/authorize" method="POST">
<div class="row">
<div class="c1 googleHeader">
<strong>step 4:</strong> Add Google account *
</div>
<div class="c1">
<input type="hidden" name="domain" value="google.com">
<input type="hidden" name="scope" value="http://www.google.com/m8/feeds/">
<input type="hidden" name="end_point_success" value="/scratch/oauth/#oauth_success_google_com">
<input type="hidden" name="end_point_auth_failure" value="/scratch/oauth/#oauth_failure_google_com">
<div class="googleActions">
<a class="skip" href="#done">skip this step</a>
<button>submit</button>
</div>
<div class="finePrint grey">
*You may delete your account at any time
</div>
</div>
</div>
</div>
</form>
</div>
<!-- done section -->
<div class="section done hidden">