diff --git a/auth/__init__.py b/auth/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5b4536cfc0c300cd323cf043495a605c3c77d1d0 --- /dev/null +++ b/auth/__init__.py @@ -0,0 +1,10 @@ + +from django.conf import settings + +TEMPLATE_BASE = settings.AUTH_TEMPLATE_BASE or "auth/templates/base.html" + +# enabled auth systems +import auth_systems +ENABLED_AUTH_SYSTEMS = settings.AUTH_ENABLED_AUTH_SYSTEMS or auth_systems.AUTH_SYSTEMS.keys() +DEFAULT_AUTH_SYSTEM = settings.AUTH_DEFAULT_AUTH_SYSTEM or None + diff --git a/auth/auth_systems/.gitignore b/auth/auth_systems/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..85c334c60e41768560181d92cd78ba65f2982762 --- /dev/null +++ b/auth/auth_systems/.gitignore @@ -0,0 +1,2 @@ +twitterconfig.py +facebookconfig.py diff --git a/auth/auth_systems/__init__.py b/auth/auth_systems/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8f13235e021eb205dbbafb5892c20322cb03bd79 --- /dev/null +++ b/auth/auth_systems/__init__.py @@ -0,0 +1,14 @@ + +AUTH_SYSTEMS = {} + +import twitter, password, cas, facebook, google, yahoo +AUTH_SYSTEMS['twitter'] = twitter +AUTH_SYSTEMS['password'] = password +AUTH_SYSTEMS['cas'] = cas +AUTH_SYSTEMS['facebook'] = facebook +AUTH_SYSTEMS['google'] = google +AUTH_SYSTEMS['yahoo'] = yahoo + +# not ready +#import live +#AUTH_SYSTEMS['live'] = live diff --git a/auth/auth_systems/cas.py b/auth/auth_systems/cas.py new file mode 100644 index 0000000000000000000000000000000000000000..24935e99dcdd4f05819d0971283e9f8905132fb3 --- /dev/null +++ b/auth/auth_systems/cas.py @@ -0,0 +1,154 @@ +""" +CAS (Princeton) Authentication + +Some code borrowed from +https://sp.princeton.edu/oit/sdp/CAS/Wiki%20Pages/Python.aspx +""" + +from django.http import * +from django.core.mail import send_mail +from django.conf import settings + +import sys, os, cgi, urllib, urllib2, re +from xml.etree import ElementTree + +CAS_EMAIL_DOMAIN = "princeton.edu" +CAS_URL= 'https://fed.princeton.edu/cas/' +CAS_LOGOUT_URL = 'https://fed.princeton.edu/cas/logout?service=%s' + +# eligibility checking +if hasattr(settings, 'CAS_USERNAME'): + CAS_USERNAME = settings.CAS_USERNAME + CAS_PASSWORD = settings.CAS_PASSWORD + CAS_ELIGIBILITY_URL = settings.CAS_ELIGIBILITY_URL + CAS_ELIGIBILITY_REALM = settings.CAS_ELIGIBILITY_REALM + +# display tweaks +LOGIN_MESSAGE = "Log in with my NetID" + +def _get_service_url(): + # FIXME current URL + from auth.views import after + from django.conf import settings + from django.core.urlresolvers import reverse + + return settings.URL_HOST + reverse(after) + +def get_auth_url(request): + return CAS_URL + 'login?service=' + urllib.quote(_get_service_url()) + +def get_user_category(user_id): + theurl = CAS_ELIGIBILITY_URL % user_id + + auth_handler = urllib2.HTTPBasicAuthHandler() + auth_handler.add_password(realm=CAS_ELIGIBILITY_REALM, uri= theurl, user= CAS_USERNAME, passwd = CAS_PASSWORD) + opener = urllib2.build_opener(auth_handler) + urllib2.install_opener(opener) + + result = urllib2.urlopen(CAS_ELIGIBILITY_URL % user_id).read().strip() + parsed_result = ElementTree.fromstring(result) + return parsed_result.text + + +def get_user_info(user_id): + url = 'http://dsml.princeton.edu/' + headers = {'SOAPAction': "#searchRequest", 'Content-Type': 'text/xml'} + + request_body = """<?xml version='1.0' encoding='UTF-8'?> + <soap-env:Envelope + xmlns:xsd='http://www.w3.org/2001/XMLSchema' + xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' + xmlns:soap-env='http://schemas.xmlsoap.org/soap/envelope/'> + <soap-env:Body> + <batchRequest xmlns='urn:oasis:names:tc:DSML:2:0:core' + requestID='searching'> + <searchRequest + dn='o=Princeton University, c=US' + scope='wholeSubtree' + derefAliases='neverDerefAliases' + sizeLimit='200'> + <filter> + <equalityMatch name='uid'> + <value>%s</value> + </equalityMatch> + </filter> + <attributes> + <attribute name="displayName"/> + <attribute name="pustatus"/> + </attributes> + </searchRequest> + </batchRequest> + </soap-env:Body> + </soap-env:Envelope> +""" % user_id + + req = urllib2.Request(url, request_body, headers) + response = urllib2.urlopen(req).read() + + # parse the result + from xml.dom.minidom import parseString + + response_doc = parseString(response) + + # get the value elements (a bit of a hack but no big deal) + values = response_doc.getElementsByTagName('value') + + return {'name' : values[0].firstChild.wholeText, 'category' : values[1].firstChild.wholeText} + +def get_user_info_after_auth(request): + ticket = request.GET.get('ticket', None) + + # if no ticket, this is a logout + if not ticket: + return None + + # fetch the information from the CAS server + val_url = CAS_URL + "validate" + \ + '?service=' + urllib.quote(_get_service_url()) + \ + '&ticket=' + urllib.quote(ticket) + r = urllib.urlopen(val_url).readlines() # returns 2 lines + + # success + if len(r) == 2 and re.match("yes", r[0]) != None: + netid = r[1].strip() + + category = get_user_category(netid) + info = {'name': netid, 'category': category} + + return {'type': 'cas', 'user_id': netid, 'name': netid, 'info': info, 'token': None} + else: + return None + +def do_logout(user): + """ + Perform logout of CAS by redirecting to the CAS logout URL + """ + return HttpResponseRedirect(CAS_LOGOUT_URL % _get_service_url()) + +def update_status(token, message): + """ + simple update + """ + pass + +def send_message(user_id, name, user_info, subject, body): + """ + send email, for now just to Princeton + """ + # if the user_id contains an @ sign already + if "@" in user_id: + email = user_id + else: + email = "%s@%s" % (user_id, CAS_EMAIL_DOMAIN) + + if user_info.has_key('name'): + name = user_info["name"] + else: + name = email + + send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (name, email)], fail_silently=False) + +def check_constraint(constraint, user_info): + if not user_info.has_key('category'): + return False + return constraint['year'] == user_info['category'] diff --git a/auth/auth_systems/facebook.py b/auth/auth_systems/facebook.py new file mode 100644 index 0000000000000000000000000000000000000000..4dc1e7406331c224f6b422bd06bb32b9325961c0 --- /dev/null +++ b/auth/auth_systems/facebook.py @@ -0,0 +1,69 @@ +""" +Facebook Authentication +""" + +import logging + +from django.conf import settings +from django.core.mail import send_mail + +APP_ID = settings.FACEBOOK_APP_ID +API_KEY = settings.FACEBOOK_API_KEY +API_SECRET = settings.FACEBOOK_API_SECRET + +#from facebookclient import Facebook +import urllib, urllib2, cgi + +# some parameters to indicate that status updating is possible +STATUS_UPDATES = True +STATUS_UPDATE_WORDING_TEMPLATE = "Send %s to your facebook status" + +from auth import utils + +def facebook_url(url, params): + if params: + return "https://graph.facebook.com%s?%s" % (url, urllib.urlencode(params)) + else: + return "https://graph.facebook.com%s" % url + +def facebook_get(url, params): + full_url = facebook_url(url,params) + return urllib2.urlopen(full_url).read() + +def facebook_post(url, params): + full_url = facebook_url(url, None) + return urllib2.urlopen(full_url, urllib.urlencode(params)).read() + +def get_auth_url(request, redirect_url): + request.session['fb_redirect_uri'] = redirect_url + return facebook_url('/oauth/authorize', { + 'client_id': APP_ID, + 'redirect_uri': redirect_url, + 'scope': 'publish_stream,email'}) + +def get_user_info_after_auth(request): + args = facebook_get('/oauth/access_token', { + 'client_id' : APP_ID, + 'redirect_uri' : request.session['fb_redirect_uri'], + 'client_secret' : API_SECRET, + 'code' : request.GET['code'] + }) + + access_token = cgi.parse_qs(args)['access_token'][0] + + info = utils.from_json(facebook_get('/me', {'access_token':access_token})) + + return {'type': 'facebook', 'user_id' : info['id'], 'name': info['name'], 'email': info['email'], 'info': info, 'token': {'access_token': access_token}} + +def update_status(user_id, user_info, token, message): + """ + post a message to the auth system's update stream, e.g. twitter stream + """ + result = facebook_post('/me/feed', { + 'access_token': token['access_token'], + 'message': message + }) + +def send_message(user_id, user_name, user_info, subject, body): + if user_info.has_key('email'): + send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (user_name, user_info['email'])], fail_silently=False) diff --git a/auth/auth_systems/facebookclient/__init__.py b/auth/auth_systems/facebookclient/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7ee9e0c37c4bf1aef85d0e1b11bb6c1dec3dfb09 --- /dev/null +++ b/auth/auth_systems/facebookclient/__init__.py @@ -0,0 +1,1431 @@ +#! /usr/bin/env python +# +# pyfacebook - Python bindings for the Facebook API +# +# Copyright (c) 2008, Samuel Cormier-Iijima +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author nor the names of its contributors may +# be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Python bindings for the Facebook API (pyfacebook - http://code.google.com/p/pyfacebook) + +PyFacebook is a client library that wraps the Facebook API. + +For more information, see + +Home Page: http://code.google.com/p/pyfacebook +Developer Wiki: http://wiki.developers.facebook.com/index.php/Python +Facebook IRC Channel: #facebook on irc.freenode.net + +PyFacebook can use simplejson if it is installed, which +is much faster than XML and also uses less bandwith. Go to +http://undefined.org/python/#simplejson to download it, or do +apt-get install python-simplejson on a Debian-like system. +""" + +import sys +import time +import struct +import urllib +import urllib2 +import httplib +try: + import hashlib +except ImportError: + import md5 as hashlib +import binascii +import urlparse +import mimetypes + +# try to use simplejson first, otherwise fallback to XML +RESPONSE_FORMAT = 'JSON' +try: + import json as simplejson +except ImportError: + try: + import simplejson + except ImportError: + try: + from django.utils import simplejson + except ImportError: + try: + import jsonlib as simplejson + simplejson.loads + except (ImportError, AttributeError): + from xml.dom import minidom + RESPONSE_FORMAT = 'XML' + +# support Google App Engine. GAE does not have a working urllib.urlopen. +try: + from google.appengine.api import urlfetch + + def urlread(url, data=None, headers=None): + if data is not None: + if headers is None: + headers = {"Content-type": "application/x-www-form-urlencoded"} + method = urlfetch.POST + else: + if headers is None: + headers = {} + method = urlfetch.GET + + result = urlfetch.fetch(url, method=method, + payload=data, headers=headers) + + if result.status_code == 200: + return result.content + else: + raise urllib2.URLError("fetch error url=%s, code=%d" % (url, result.status_code)) + +except ImportError: + def urlread(url, data=None): + res = urllib2.urlopen(url, data=data) + return res.read() + +__all__ = ['Facebook'] + +VERSION = '0.1' + +FACEBOOK_URL = 'http://api.facebook.com/restserver.php' +FACEBOOK_SECURE_URL = 'https://api.facebook.com/restserver.php' + +class json(object): pass + +# simple IDL for the Facebook API +METHODS = { + 'application': { + 'getPublicInfo': [ + ('application_id', int, ['optional']), + ('application_api_key', str, ['optional']), + ('application_canvas_name', str,['optional']), + ], + }, + + # admin methods + 'admin': { + 'getAllocation': [ + ('integration_point_name', str, []), + ], + }, + + # auth methods + 'auth': { + 'revokeAuthorization': [ + ('uid', int, ['optional']), + ], + }, + + # feed methods + 'feed': { + 'publishStoryToUser': [ + ('title', str, []), + ('body', str, ['optional']), + ('image_1', str, ['optional']), + ('image_1_link', str, ['optional']), + ('image_2', str, ['optional']), + ('image_2_link', str, ['optional']), + ('image_3', str, ['optional']), + ('image_3_link', str, ['optional']), + ('image_4', str, ['optional']), + ('image_4_link', str, ['optional']), + ('priority', int, ['optional']), + ], + + 'publishActionOfUser': [ + ('title', str, []), + ('body', str, ['optional']), + ('image_1', str, ['optional']), + ('image_1_link', str, ['optional']), + ('image_2', str, ['optional']), + ('image_2_link', str, ['optional']), + ('image_3', str, ['optional']), + ('image_3_link', str, ['optional']), + ('image_4', str, ['optional']), + ('image_4_link', str, ['optional']), + ('priority', int, ['optional']), + ], + + 'publishTemplatizedAction': [ + ('title_template', str, []), + ('page_actor_id', int, ['optional']), + ('title_data', json, ['optional']), + ('body_template', str, ['optional']), + ('body_data', json, ['optional']), + ('body_general', str, ['optional']), + ('image_1', str, ['optional']), + ('image_1_link', str, ['optional']), + ('image_2', str, ['optional']), + ('image_2_link', str, ['optional']), + ('image_3', str, ['optional']), + ('image_3_link', str, ['optional']), + ('image_4', str, ['optional']), + ('image_4_link', str, ['optional']), + ('target_ids', list, ['optional']), + ], + + 'registerTemplateBundle': [ + ('one_line_story_templates', json, []), + ('short_story_templates', json, ['optional']), + ('full_story_template', json, ['optional']), + ('action_links', json, ['optional']), + ], + + 'deactivateTemplateBundleByID': [ + ('template_bundle_id', int, []), + ], + + 'getRegisteredTemplateBundles': [], + + 'getRegisteredTemplateBundleByID': [ + ('template_bundle_id', str, []), + ], + + 'publishUserAction': [ + ('template_bundle_id', int, []), + ('template_data', json, ['optional']), + ('target_ids', list, ['optional']), + ('body_general', str, ['optional']), + ('story_size', int, ['optional']), + ], + }, + + # fql methods + 'fql': { + 'query': [ + ('query', str, []), + ], + }, + + # friends methods + 'friends': { + 'areFriends': [ + ('uids1', list, []), + ('uids2', list, []), + ], + + 'get': [ + ('flid', int, ['optional']), + ], + + 'getLists': [], + + 'getAppUsers': [], + }, + + # notifications methods + 'notifications': { + 'get': [], + + 'send': [ + ('to_ids', list, []), + ('notification', str, []), + ('email', str, ['optional']), + ('type', str, ['optional']), + ], + + 'sendRequest': [ + ('to_ids', list, []), + ('type', str, []), + ('content', str, []), + ('image', str, []), + ('invite', bool, []), + ], + + 'sendEmail': [ + ('recipients', list, []), + ('subject', str, []), + ('text', str, ['optional']), + ('fbml', str, ['optional']), + ] + }, + + # profile methods + 'profile': { + 'setFBML': [ + ('markup', str, ['optional']), + ('uid', int, ['optional']), + ('profile', str, ['optional']), + ('profile_action', str, ['optional']), + ('mobile_fbml', str, ['optional']), + ('profile_main', str, ['optional']), + ], + + 'getFBML': [ + ('uid', int, ['optional']), + ('type', int, ['optional']), + ], + + 'setInfo': [ + ('title', str, []), + ('type', int, []), + ('info_fields', json, []), + ('uid', int, []), + ], + + 'getInfo': [ + ('uid', int, []), + ], + + 'setInfoOptions': [ + ('field', str, []), + ('options', json, []), + ], + + 'getInfoOptions': [ + ('field', str, []), + ], + }, + + # users methods + 'users': { + 'getInfo': [ + ('uids', list, []), + ('fields', list, [('default', ['name'])]), + ], + + 'getStandardInfo': [ + ('uids', list, []), + ('fields', list, [('default', ['uid'])]), + ], + + 'getLoggedInUser': [], + + 'isAppAdded': [], + + 'hasAppPermission': [ + ('ext_perm', str, []), + ('uid', int, ['optional']), + ], + + 'setStatus': [ + ('status', str, []), + ('clear', bool, []), + ('status_includes_verb', bool, ['optional']), + ('uid', int, ['optional']), + ], + }, + + # events methods + 'events': { + 'get': [ + ('uid', int, ['optional']), + ('eids', list, ['optional']), + ('start_time', int, ['optional']), + ('end_time', int, ['optional']), + ('rsvp_status', str, ['optional']), + ], + + 'getMembers': [ + ('eid', int, []), + ], + + 'create': [ + ('event_info', json, []), + ], + }, + + # update methods + 'update': { + 'decodeIDs': [ + ('ids', list, []), + ], + }, + + # groups methods + 'groups': { + 'get': [ + ('uid', int, ['optional']), + ('gids', list, ['optional']), + ], + + 'getMembers': [ + ('gid', int, []), + ], + }, + + # marketplace methods + 'marketplace': { + 'createListing': [ + ('listing_id', int, []), + ('show_on_profile', bool, []), + ('listing_attrs', str, []), + ], + + 'getCategories': [], + + 'getListings': [ + ('listing_ids', list, []), + ('uids', list, []), + ], + + 'getSubCategories': [ + ('category', str, []), + ], + + 'removeListing': [ + ('listing_id', int, []), + ('status', str, []), + ], + + 'search': [ + ('category', str, ['optional']), + ('subcategory', str, ['optional']), + ('query', str, ['optional']), + ], + }, + + # pages methods + 'pages': { + 'getInfo': [ + ('fields', list, [('default', ['page_id', 'name'])]), + ('page_ids', list, ['optional']), + ('uid', int, ['optional']), + ], + + 'isAdmin': [ + ('page_id', int, []), + ], + + 'isAppAdded': [ + ('page_id', int, []), + ], + + 'isFan': [ + ('page_id', int, []), + ('uid', int, []), + ], + }, + + # photos methods + 'photos': { + 'addTag': [ + ('pid', int, []), + ('tag_uid', int, [('default', 0)]), + ('tag_text', str, [('default', '')]), + ('x', float, [('default', 50)]), + ('y', float, [('default', 50)]), + ('tags', str, ['optional']), + ], + + 'createAlbum': [ + ('name', str, []), + ('location', str, ['optional']), + ('description', str, ['optional']), + ], + + 'get': [ + ('subj_id', int, ['optional']), + ('aid', int, ['optional']), + ('pids', list, ['optional']), + ], + + 'getAlbums': [ + ('uid', int, ['optional']), + ('aids', list, ['optional']), + ], + + 'getTags': [ + ('pids', list, []), + ], + }, + + # status methods + 'status': { + 'get': [ + ('uid', int, ['optional']), + ('limit', int, ['optional']), + ], + 'set': [ + ('status', str, ['optional']), + ('uid', int, ['optional']), + ], + }, + + # fbml methods + 'fbml': { + 'refreshImgSrc': [ + ('url', str, []), + ], + + 'refreshRefUrl': [ + ('url', str, []), + ], + + 'setRefHandle': [ + ('handle', str, []), + ('fbml', str, []), + ], + }, + + # SMS Methods + 'sms' : { + 'canSend' : [ + ('uid', int, []), + ], + + 'send' : [ + ('uid', int, []), + ('message', str, []), + ('session_id', int, []), + ('req_session', bool, []), + ], + }, + + 'data': { + 'getCookies': [ + ('uid', int, []), + ('string', str, ['optional']), + ], + + 'setCookie': [ + ('uid', int, []), + ('name', str, []), + ('value', str, []), + ('expires', int, ['optional']), + ('path', str, ['optional']), + ], + }, + + # connect methods + 'connect': { + 'registerUsers': [ + ('accounts', json, []), + ], + + 'unregisterUsers': [ + ('email_hashes', json, []), + ], + + 'getUnconnectedFriendsCount': [ + ], + }, + + #stream methods (beta) + 'stream' : { + 'addComment' : [ + ('post_id', int, []), + ('comment', str, []), + ('uid', int, ['optional']), + ], + + 'addLike': [ + ('uid', int, ['optional']), + ('post_id', int, ['optional']), + ], + + 'get' : [ + ('viewer_id', int, ['optional']), + ('source_ids', list, ['optional']), + ('start_time', int, ['optional']), + ('end_time', int, ['optional']), + ('limit', int, ['optional']), + ('filter_key', str, ['optional']), + ], + + 'getComments' : [ + ('post_id', int, []), + ], + + 'getFilters' : [ + ('uid', int, ['optional']), + ], + + 'publish' : [ + ('message', str, ['optional']), + ('attachment', json, ['optional']), + ('action_links', json, ['optional']), + ('target_id', str, ['optional']), + ('uid', str, ['optional']), + ], + + 'remove' : [ + ('post_id', int, []), + ('uid', int, ['optional']), + ], + + 'removeComment' : [ + ('comment_id', int, []), + ('uid', int, ['optional']), + ], + + 'removeLike' : [ + ('uid', int, ['optional']), + ('post_id', int, ['optional']), + ], + } +} + +class Proxy(object): + """Represents a "namespace" of Facebook API calls.""" + + def __init__(self, client, name): + self._client = client + self._name = name + + def __call__(self, method=None, args=None, add_session_args=True): + # for Django templates + if method is None: + return self + + if add_session_args: + self._client._add_session_args(args) + + return self._client('%s.%s' % (self._name, method), args) + + +# generate the Facebook proxies +def __generate_proxies(): + for namespace in METHODS: + methods = {} + + for method in METHODS[namespace]: + params = ['self'] + body = ['args = {}'] + + for param_name, param_type, param_options in METHODS[namespace][method]: + param = param_name + + for option in param_options: + if isinstance(option, tuple) and option[0] == 'default': + if param_type == list: + param = '%s=None' % param_name + body.append('if %s is None: %s = %s' % (param_name, param_name, repr(option[1]))) + else: + param = '%s=%s' % (param_name, repr(option[1])) + + if param_type == json: + # we only jsonify the argument if it's a list or a dict, for compatibility + body.append('if isinstance(%s, list) or isinstance(%s, dict): %s = simplejson.dumps(%s)' % ((param_name,) * 4)) + + if 'optional' in param_options: + param = '%s=None' % param_name + body.append('if %s is not None: args[\'%s\'] = %s' % (param_name, param_name, param_name)) + else: + body.append('args[\'%s\'] = %s' % (param_name, param_name)) + + params.append(param) + + # simple docstring to refer them to Facebook API docs + body.insert(0, '"""Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=%s.%s"""' % (namespace, method)) + + body.insert(0, 'def %s(%s):' % (method, ', '.join(params))) + + body.append('return self(\'%s\', args)' % method) + + exec('\n '.join(body)) + + methods[method] = eval(method) + + proxy = type('%sProxy' % namespace.title(), (Proxy, ), methods) + + globals()[proxy.__name__] = proxy + + +__generate_proxies() + + +class FacebookError(Exception): + """Exception class for errors received from Facebook.""" + + def __init__(self, code, msg, args=None): + self.code = code + self.msg = msg + self.args = args + + def __str__(self): + return 'Error %s: %s' % (self.code, self.msg) + + +class AuthProxy(AuthProxy): + """Special proxy for facebook.auth.""" + + def getSession(self): + """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.getSession""" + args = {} + try: + args['auth_token'] = self._client.auth_token + except AttributeError: + raise RuntimeError('Client does not have auth_token set.') + result = self._client('%s.getSession' % self._name, args) + self._client.session_key = result['session_key'] + self._client.uid = result['uid'] + self._client.secret = result.get('secret') + self._client.session_key_expires = result['expires'] + return result + + def createToken(self): + """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.createToken""" + token = self._client('%s.createToken' % self._name) + self._client.auth_token = token + return token + + +class FriendsProxy(FriendsProxy): + """Special proxy for facebook.friends.""" + + def get(self, **kwargs): + """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=friends.get""" + if not kwargs.get('flid') and self._client._friends: + return self._client._friends + return super(FriendsProxy, self).get(**kwargs) + + +class PhotosProxy(PhotosProxy): + """Special proxy for facebook.photos.""" + + def upload(self, image, aid=None, caption=None, size=(604, 1024), filename=None, callback=None): + """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=photos.upload + + size -- an optional size (width, height) to resize the image to before uploading. Resizes by default + to Facebook's maximum display width of 604. + """ + args = {} + + if aid is not None: + args['aid'] = aid + + if caption is not None: + args['caption'] = caption + + args = self._client._build_post_args('facebook.photos.upload', self._client._add_session_args(args)) + + try: + import cStringIO as StringIO + except ImportError: + import StringIO + + # check for a filename specified...if the user is passing binary data in + # image then a filename will be specified + if filename is None: + try: + import Image + except ImportError: + data = StringIO.StringIO(open(image, 'rb').read()) + else: + img = Image.open(image) + if size: + img.thumbnail(size, Image.ANTIALIAS) + data = StringIO.StringIO() + img.save(data, img.format) + else: + # there was a filename specified, which indicates that image was not + # the path to an image file but rather the binary data of a file + data = StringIO.StringIO(image) + image = filename + + content_type, body = self.__encode_multipart_formdata(list(args.iteritems()), [(image, data)]) + urlinfo = urlparse.urlsplit(self._client.facebook_url) + try: + content_length = len(body) + chunk_size = 4096 + + h = httplib.HTTPConnection(urlinfo[1]) + h.putrequest('POST', urlinfo[2]) + h.putheader('Content-Type', content_type) + h.putheader('Content-Length', str(content_length)) + h.putheader('MIME-Version', '1.0') + h.putheader('User-Agent', 'PyFacebook Client Library') + h.endheaders() + + if callback: + count = 0 + while len(body) > 0: + if len(body) < chunk_size: + data = body + body = '' + else: + data = body[0:chunk_size] + body = body[chunk_size:] + + h.send(data) + count += 1 + callback(count, chunk_size, content_length) + else: + h.send(body) + + response = h.getresponse() + + if response.status != 200: + raise Exception('Error uploading photo: Facebook returned HTTP %s (%s)' % (response.status, response.reason)) + response = response.read() + except: + # sending the photo failed, perhaps we are using GAE + try: + from google.appengine.api import urlfetch + + try: + response = urlread(url=self._client.facebook_url,data=body,headers={'POST':urlinfo[2],'Content-Type':content_type,'MIME-Version':'1.0'}) + except urllib2.URLError: + raise Exception('Error uploading photo: Facebook returned %s' % (response)) + except ImportError: + # could not import from google.appengine.api, so we are not running in GAE + raise Exception('Error uploading photo.') + + return self._client._parse_response(response, 'facebook.photos.upload') + + + def __encode_multipart_formdata(self, fields, files): + """Encodes a multipart/form-data message to upload an image.""" + boundary = '-------tHISiStheMulTIFoRMbOUNDaRY' + crlf = '\r\n' + l = [] + + for (key, value) in fields: + l.append('--' + boundary) + l.append('Content-Disposition: form-data; name="%s"' % str(key)) + l.append('') + l.append(str(value)) + for (filename, value) in files: + l.append('--' + boundary) + l.append('Content-Disposition: form-data; filename="%s"' % (str(filename), )) + l.append('Content-Type: %s' % self.__get_content_type(filename)) + l.append('') + l.append(value.getvalue()) + l.append('--' + boundary + '--') + l.append('') + body = crlf.join(l) + content_type = 'multipart/form-data; boundary=%s' % boundary + return content_type, body + + + def __get_content_type(self, filename): + """Returns a guess at the MIME type of the file from the filename.""" + return str(mimetypes.guess_type(filename)[0]) or 'application/octet-stream' + + +class Facebook(object): + """ + Provides access to the Facebook API. + + Instance Variables: + + added + True if the user has added this application. + + api_key + Your API key, as set in the constructor. + + app_name + Your application's name, i.e. the APP_NAME in http://apps.facebook.com/APP_NAME/ if + this is for an internal web application. Optional, but useful for automatic redirects + to canvas pages. + + auth_token + The auth token that Facebook gives you, either with facebook.auth.createToken, + or through a GET parameter. + + callback_path + The path of the callback set in the Facebook app settings. If your callback is set + to http://www.example.com/facebook/callback/, this should be '/facebook/callback/'. + Optional, but useful for automatic redirects back to the same page after login. + + desktop + True if this is a desktop app, False otherwise. Used for determining how to + authenticate. + + ext_perms + Any extended permissions that the user has granted to your application. + This parameter is set only if the user has granted any. + + facebook_url + The url to use for Facebook requests. + + facebook_secure_url + The url to use for secure Facebook requests. + + in_canvas + True if the current request is for a canvas page. + + in_iframe + True if the current request is for an HTML page to embed in Facebook inside an iframe. + + is_session_from_cookie + True if the current request session comes from a session cookie. + + in_profile_tab + True if the current request is for a user's tab for your application. + + internal + True if this Facebook object is for an internal application (one that can be added on Facebook) + + locale + The user's locale. Default: 'en_US' + + page_id + Set to the page_id of the current page (if any) + + profile_update_time + The time when this user's profile was last updated. This is a UNIX timestamp. Default: None if unknown. + + secret + Secret that is used after getSession for desktop apps. + + secret_key + Your application's secret key, as set in the constructor. + + session_key + The current session key. Set automatically by auth.getSession, but can be set + manually for doing infinite sessions. + + session_key_expires + The UNIX time of when this session key expires, or 0 if it never expires. + + uid + After a session is created, you can get the user's UID with this variable. Set + automatically by auth.getSession. + + ---------------------------------------------------------------------- + + """ + + def __init__(self, api_key, secret_key, auth_token=None, app_name=None, callback_path=None, internal=None, proxy=None, facebook_url=None, facebook_secure_url=None): + """ + Initializes a new Facebook object which provides wrappers for the Facebook API. + + If this is a desktop application, the next couple of steps you might want to take are: + + facebook.auth.createToken() # create an auth token + facebook.login() # show a browser window + wait_login() # somehow wait for the user to log in + facebook.auth.getSession() # get a session key + + For web apps, if you are passed an auth_token from Facebook, pass that in as a named parameter. + Then call: + + facebook.auth.getSession() + + """ + self.api_key = api_key + self.secret_key = secret_key + self.session_key = None + self.session_key_expires = None + self.auth_token = auth_token + self.secret = None + self.uid = None + self.page_id = None + self.in_canvas = False + self.in_iframe = False + self.is_session_from_cookie = False + self.in_profile_tab = False + self.added = False + self.app_name = app_name + self.callback_path = callback_path + self.internal = internal + self._friends = None + self.locale = 'en_US' + self.profile_update_time = None + self.ext_perms = None + self.proxy = proxy + if facebook_url is None: + self.facebook_url = FACEBOOK_URL + else: + self.facebook_url = facebook_url + if facebook_secure_url is None: + self.facebook_secure_url = FACEBOOK_SECURE_URL + else: + self.facebook_secure_url = facebook_secure_url + + for namespace in METHODS: + self.__dict__[namespace] = eval('%sProxy(self, \'%s\')' % (namespace.title(), 'facebook.%s' % namespace)) + + + def _hash_args(self, args, secret=None): + """Hashes arguments by joining key=value pairs, appending a secret, and then taking the MD5 hex digest.""" + # @author: houyr + # fix for UnicodeEncodeError + hasher = hashlib.md5(''.join(['%s=%s' % (isinstance(x, unicode) and x.encode("utf-8") or x, isinstance(args[x], unicode) and args[x].encode("utf-8") or args[x]) for x in sorted(args.keys())])) + if secret: + hasher.update(secret) + elif self.secret: + hasher.update(self.secret) + else: + hasher.update(self.secret_key) + return hasher.hexdigest() + + + def _parse_response_item(self, node): + """Parses an XML response node from Facebook.""" + if node.nodeType == node.DOCUMENT_NODE and \ + node.childNodes[0].hasAttributes() and \ + node.childNodes[0].hasAttribute('list') and \ + node.childNodes[0].getAttribute('list') == "true": + return {node.childNodes[0].nodeName: self._parse_response_list(node.childNodes[0])} + elif node.nodeType == node.ELEMENT_NODE and \ + node.hasAttributes() and \ + node.hasAttribute('list') and \ + node.getAttribute('list')=="true": + return self._parse_response_list(node) + elif len(filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes)) > 0: + return self._parse_response_dict(node) + else: + return ''.join(node.data for node in node.childNodes if node.nodeType == node.TEXT_NODE) + + + def _parse_response_dict(self, node): + """Parses an XML dictionary response node from Facebook.""" + result = {} + for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes): + result[item.nodeName] = self._parse_response_item(item) + if node.nodeType == node.ELEMENT_NODE and node.hasAttributes(): + if node.hasAttribute('id'): + result['id'] = node.getAttribute('id') + return result + + + def _parse_response_list(self, node): + """Parses an XML list response node from Facebook.""" + result = [] + for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes): + result.append(self._parse_response_item(item)) + return result + + + def _check_error(self, response): + """Checks if the given Facebook response is an error, and then raises the appropriate exception.""" + if type(response) is dict and response.has_key('error_code'): + raise FacebookError(response['error_code'], response['error_msg'], response['request_args']) + + + def _build_post_args(self, method, args=None): + """Adds to args parameters that are necessary for every call to the API.""" + if args is None: + args = {} + + for arg in args.items(): + if type(arg[1]) == list: + args[arg[0]] = ','.join(str(a) for a in arg[1]) + elif type(arg[1]) == unicode: + args[arg[0]] = arg[1].encode("UTF-8") + elif type(arg[1]) == bool: + args[arg[0]] = str(arg[1]).lower() + + args['method'] = method + args['api_key'] = self.api_key + args['v'] = '1.0' + args['format'] = RESPONSE_FORMAT + args['sig'] = self._hash_args(args) + + return args + + + def _add_session_args(self, args=None): + """Adds 'session_key' and 'call_id' to args, which are used for API calls that need sessions.""" + if args is None: + args = {} + + if not self.session_key: + return args + #some calls don't need a session anymore. this might be better done in the markup + #raise RuntimeError('Session key not set. Make sure auth.getSession has been called.') + + args['session_key'] = self.session_key + args['call_id'] = str(int(time.time() * 1000)) + + return args + + + def _parse_response(self, response, method, format=None): + """Parses the response according to the given (optional) format, which should be either 'JSON' or 'XML'.""" + if not format: + format = RESPONSE_FORMAT + + if format == 'JSON': + result = simplejson.loads(response) + + self._check_error(result) + elif format == 'XML': + dom = minidom.parseString(response) + result = self._parse_response_item(dom) + dom.unlink() + + if 'error_response' in result: + self._check_error(result['error_response']) + + result = result[method[9:].replace('.', '_') + '_response'] + else: + raise RuntimeError('Invalid format specified.') + + return result + + + def hash_email(self, email): + """ + Hash an email address in a format suitable for Facebook Connect. + + """ + email = email.lower().strip() + return "%s_%s" % ( + struct.unpack("I", struct.pack("i", binascii.crc32(email)))[0], + hashlib.md5(email).hexdigest(), + ) + + + def unicode_urlencode(self, params): + """ + @author: houyr + A unicode aware version of urllib.urlencode. + """ + if isinstance(params, dict): + params = params.items() + return urllib.urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v) + for k, v in params]) + + + def __call__(self, method=None, args=None, secure=False): + """Make a call to Facebook's REST server.""" + # for Django templates, if this object is called without any arguments + # return the object itself + if method is None: + return self + + # __init__ hard-codes into en_US + if args is not None and not args.has_key('locale'): + args['locale'] = self.locale + + # @author: houyr + # fix for bug of UnicodeEncodeError + post_data = self.unicode_urlencode(self._build_post_args(method, args)) + + if self.proxy: + proxy_handler = urllib2.ProxyHandler(self.proxy) + opener = urllib2.build_opener(proxy_handler) + if secure: + response = opener.open(self.facebook_secure_url, post_data).read() + else: + response = opener.open(self.facebook_url, post_data).read() + else: + if secure: + response = urlread(self.facebook_secure_url, post_data) + else: + response = urlread(self.facebook_url, post_data) + + return self._parse_response(response, method) + + + # URL helpers + def get_url(self, page, **args): + """ + Returns one of the Facebook URLs (www.facebook.com/SOMEPAGE.php). + Named arguments are passed as GET query string parameters. + + """ + return 'http://www.facebook.com/%s.php?%s' % (page, urllib.urlencode(args)) + + + def get_app_url(self, path=''): + """ + Returns the URL for this app's canvas page, according to app_name. + + """ + return 'http://apps.facebook.com/%s/%s' % (self.app_name, path) + + + def get_add_url(self, next=None): + """ + Returns the URL that the user should be redirected to in order to add the application. + + """ + args = {'api_key': self.api_key, 'v': '1.0'} + + if next is not None: + args['next'] = next + + return self.get_url('install', **args) + + + def get_authorize_url(self, next=None, next_cancel=None): + """ + Returns the URL that the user should be redirected to in order to + authorize certain actions for application. + + """ + args = {'api_key': self.api_key, 'v': '1.0'} + + if next is not None: + args['next'] = next + + if next_cancel is not None: + args['next_cancel'] = next_cancel + + return self.get_url('authorize', **args) + + + def get_login_url(self, next=None, popup=False, canvas=True): + """ + Returns the URL that the user should be redirected to in order to login. + + next -- the URL that Facebook should redirect to after login + + """ + args = {'api_key': self.api_key, 'v': '1.0'} + + if next is not None: + args['next'] = next + + if canvas is True: + args['canvas'] = 1 + + if popup is True: + args['popup'] = 1 + + if self.auth_token is not None: + args['auth_token'] = self.auth_token + + return self.get_url('login', **args) + + + def login(self, popup=False): + """Open a web browser telling the user to login to Facebook.""" + import webbrowser + webbrowser.open(self.get_login_url(popup=popup)) + + + def get_ext_perm_url(self, ext_perm, next=None, popup=False): + """ + Returns the URL that the user should be redirected to in order to grant an extended permission. + + ext_perm -- the name of the extended permission to request + next -- the URL that Facebook should redirect to after login + + """ + args = {'ext_perm': ext_perm, 'api_key': self.api_key, 'v': '1.0'} + + if next is not None: + args['next'] = next + + if popup is True: + args['popup'] = 1 + + return self.get_url('authorize', **args) + + + def request_extended_permission(self, ext_perm, popup=False): + """Open a web browser telling the user to grant an extended permission.""" + import webbrowser + webbrowser.open(self.get_ext_perm_url(ext_perm, popup=popup)) + + + def check_session(self, request): + """ + Checks the given Django HttpRequest for Facebook parameters such as + POST variables or an auth token. If the session is valid, returns True + and this object can now be used to access the Facebook API. Otherwise, + it returns False, and the application should take the appropriate action + (either log the user in or have him add the application). + + """ + self.in_canvas = (request.POST.get('fb_sig_in_canvas') == '1') + + if self.session_key and (self.uid or self.page_id): + return True + + + if request.method == 'POST': + params = self.validate_signature(request.POST) + else: + if 'installed' in request.GET: + self.added = True + + if 'fb_page_id' in request.GET: + self.page_id = request.GET['fb_page_id'] + + if 'auth_token' in request.GET: + self.auth_token = request.GET['auth_token'] + + try: + self.auth.getSession() + except FacebookError, e: + self.auth_token = None + return False + + return True + + params = self.validate_signature(request.GET) + + if not params: + # first check if we are in django - to check cookies + if hasattr(request, 'COOKIES'): + params = self.validate_cookie_signature(request.COOKIES) + self.is_session_from_cookie = True + else: + # if not, then we might be on GoogleAppEngine, check their request object cookies + if hasattr(request,'cookies'): + params = self.validate_cookie_signature(request.cookies) + self.is_session_from_cookie = True + + if not params: + return False + + if params.get('in_canvas') == '1': + self.in_canvas = True + + if params.get('in_iframe') == '1': + self.in_iframe = True + + if params.get('in_profile_tab') == '1': + self.in_profile_tab = True + + if params.get('added') == '1': + self.added = True + + if params.get('expires'): + self.session_key_expires = int(params['expires']) + + if 'locale' in params: + self.locale = params['locale'] + + if 'profile_update_time' in params: + try: + self.profile_update_time = int(params['profile_update_time']) + except ValueError: + pass + + if 'ext_perms' in params: + self.ext_perms = params['ext_perms'] + + if 'friends' in params: + if params['friends']: + self._friends = params['friends'].split(',') + else: + self._friends = [] + + if 'session_key' in params: + self.session_key = params['session_key'] + if 'user' in params: + self.uid = params['user'] + elif 'page_id' in params: + self.page_id = params['page_id'] + else: + return False + elif 'profile_session_key' in params: + self.session_key = params['profile_session_key'] + if 'profile_user' in params: + self.uid = params['profile_user'] + else: + return False + elif 'canvas_user' in params: + self.uid = params['canvas_user'] + elif 'uninstall' in params: + self.uid = params['user'] + else: + return False + + return True + + + def validate_signature(self, post, prefix='fb_sig', timeout=None): + """ + Validate parameters passed to an internal Facebook app from Facebook. + + """ + args = post.copy() + + if prefix not in args: + return None + + del args[prefix] + + if timeout and '%s_time' % prefix in post and time.time() - float(post['%s_time' % prefix]) > timeout: + return None + + args = dict([(key[len(prefix + '_'):], value) for key, value in args.items() if key.startswith(prefix)]) + + hash = self._hash_args(args) + + if hash == post[prefix]: + return args + else: + return None + + def validate_cookie_signature(self, cookies): + """ + Validate parameters passed by cookies, namely facebookconnect or js api. + """ + + api_key = self.api_key + if api_key not in cookies: + return None + + prefix = api_key + "_" + + params = {} + vals = '' + for k in sorted(cookies): + if k.startswith(prefix): + key = k.replace(prefix,"") + value = cookies[k] + params[key] = value + vals += '%s=%s' % (key, value) + + hasher = hashlib.md5(vals) + + hasher.update(self.secret_key) + digest = hasher.hexdigest() + if digest == cookies[api_key]: + params['is_session_from_cookie'] = True + return params + else: + return False + + + + +if __name__ == '__main__': + # sample desktop application + + api_key = '' + secret_key = '' + + facebook = Facebook(api_key, secret_key) + + facebook.auth.createToken() + + # Show login window + # Set popup=True if you want login without navigational elements + facebook.login() + + # Login to the window, then press enter + print 'After logging in, press enter...' + raw_input() + + facebook.auth.getSession() + print 'Session Key: ', facebook.session_key + print 'Your UID: ', facebook.uid + + info = facebook.users.getInfo([facebook.uid], ['name', 'birthday', 'affiliations', 'sex'])[0] + + print 'Your Name: ', info['name'] + print 'Your Birthday: ', info['birthday'] + print 'Your Gender: ', info['sex'] + + friends = facebook.friends.get() + friends = facebook.users.getInfo(friends[0:5], ['name', 'birthday', 'relationship_status']) + + for friend in friends: + print friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status'] + + arefriends = facebook.friends.areFriends([friends[0]['uid']], [friends[1]['uid']]) + + photos = facebook.photos.getAlbums(facebook.uid) + diff --git a/auth/auth_systems/facebookclient/djangofb/__init__.py b/auth/auth_systems/facebookclient/djangofb/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..68b1b27c37d19e1372369837006947b4f60ef7c5 --- /dev/null +++ b/auth/auth_systems/facebookclient/djangofb/__init__.py @@ -0,0 +1,248 @@ +import re +import datetime +import facebook + +from django.http import HttpResponse, HttpResponseRedirect +from django.core.exceptions import ImproperlyConfigured +from django.conf import settings +from datetime import datetime + +try: + from threading import local +except ImportError: + from django.utils._threading_local import local + +__all__ = ['Facebook', 'FacebookMiddleware', 'get_facebook_client', 'require_login', 'require_add'] + +_thread_locals = local() + +class Facebook(facebook.Facebook): + def redirect(self, url): + """ + Helper for Django which redirects to another page. If inside a + canvas page, writes a <fb:redirect> instead to achieve the same effect. + + """ + if self.in_canvas: + return HttpResponse('<fb:redirect url="%s" />' % (url, )) + elif re.search("^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?", url.lower()): + return HttpResponse('<script type="text/javascript">\ntop.location.href = "%s";\n</script>' % url) + else: + return HttpResponseRedirect(url) + + +def get_facebook_client(): + """ + Get the current Facebook object for the calling thread. + + """ + try: + return _thread_locals.facebook + except AttributeError: + raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.') + + +def require_login(next=None, internal=None): + """ + Decorator for Django views that requires the user to be logged in. + The FacebookMiddleware must be installed. + + Standard usage: + @require_login() + def some_view(request): + ... + + Redirecting after login: + To use the 'next' parameter to redirect to a specific page after login, a callable should + return a path relative to the Post-add URL. 'next' can also be an integer specifying how many + parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None, + settings.callback_path and settings.app_name are checked to redirect to the same page after logging + in. (This is the default behavior.) + @require_login(next=some_callable) + def some_view(request): + ... + """ + def decorator(view): + def newview(request, *args, **kwargs): + next = newview.next + internal = newview.internal + + try: + fb = request.facebook + except: + raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.') + + if internal is None: + internal = request.facebook.internal + + if callable(next): + next = next(request.path) + elif isinstance(next, int): + next = '/'.join(request.path.split('/')[next + 1:]) + elif next is None and fb.callback_path and request.path.startswith(fb.callback_path): + next = request.path[len(fb.callback_path):] + elif not isinstance(next, str): + next = '' + + if not fb.check_session(request): + #If user has never logged in before, the get_login_url will redirect to the TOS page + return fb.redirect(fb.get_login_url(next=next)) + + if internal and request.method == 'GET' and fb.app_name: + return fb.redirect('%s%s' % (fb.get_app_url(), next)) + + return view(request, *args, **kwargs) + newview.next = next + newview.internal = internal + return newview + return decorator + + +def require_add(next=None, internal=None, on_install=None): + """ + Decorator for Django views that requires application installation. + The FacebookMiddleware must be installed. + + Standard usage: + @require_add() + def some_view(request): + ... + + Redirecting after installation: + To use the 'next' parameter to redirect to a specific page after login, a callable should + return a path relative to the Post-add URL. 'next' can also be an integer specifying how many + parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None, + settings.callback_path and settings.app_name are checked to redirect to the same page after logging + in. (This is the default behavior.) + @require_add(next=some_callable) + def some_view(request): + ... + + Post-install processing: + Set the on_install parameter to a callable in order to handle special post-install processing. + The callable should take a request object as the parameter. + @require_add(on_install=some_callable) + def some_view(request): + ... + """ + def decorator(view): + def newview(request, *args, **kwargs): + next = newview.next + internal = newview.internal + + try: + fb = request.facebook + except: + raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.') + + if internal is None: + internal = request.facebook.internal + + if callable(next): + next = next(request.path) + elif isinstance(next, int): + next = '/'.join(request.path.split('/')[next + 1:]) + elif next is None and fb.callback_path and request.path.startswith(fb.callback_path): + next = request.path[len(fb.callback_path):] + else: + next = '' + + if not fb.check_session(request): + if fb.added: + if request.method == 'GET' and fb.app_name: + return fb.redirect('%s%s' % (fb.get_app_url(), next)) + return fb.redirect(fb.get_login_url(next=next)) + else: + return fb.redirect(fb.get_add_url(next=next)) + + if not fb.added: + return fb.redirect(fb.get_add_url(next=next)) + + if 'installed' in request.GET and callable(on_install): + on_install(request) + + if internal and request.method == 'GET' and fb.app_name: + return fb.redirect('%s%s' % (fb.get_app_url(), next)) + + return view(request, *args, **kwargs) + newview.next = next + newview.internal = internal + return newview + return decorator + +# try to preserve the argspecs +try: + import decorator +except ImportError: + pass +else: + def updater(f): + def updated(*args, **kwargs): + original = f(*args, **kwargs) + def newdecorator(view): + return decorator.new_wrapper(original(view), view) + return decorator.new_wrapper(newdecorator, original) + return decorator.new_wrapper(updated, f) + require_login = updater(require_login) + require_add = updater(require_add) + +class FacebookMiddleware(object): + """ + Middleware that attaches a Facebook object to every incoming request. + The Facebook object created can also be accessed from models for the + current thread by using get_facebook_client(). + + """ + + def __init__(self, api_key=None, secret_key=None, app_name=None, callback_path=None, internal=None): + self.api_key = api_key or settings.FACEBOOK_API_KEY + self.secret_key = secret_key or settings.FACEBOOK_SECRET_KEY + self.app_name = app_name or getattr(settings, 'FACEBOOK_APP_NAME', None) + self.callback_path = callback_path or getattr(settings, 'FACEBOOK_CALLBACK_PATH', None) + self.internal = internal or getattr(settings, 'FACEBOOK_INTERNAL', True) + self.proxy = None + if getattr(settings, 'USE_HTTP_PROXY', False): + self.proxy = settings.HTTP_PROXY + + def process_request(self, request): + _thread_locals.facebook = request.facebook = Facebook(self.api_key, self.secret_key, app_name=self.app_name, callback_path=self.callback_path, internal=self.internal, proxy=self.proxy) + if not self.internal: + if 'fb_sig_session_key' in request.GET and 'fb_sig_user' in request.GET: + request.facebook.session_key = request.session['facebook_session_key'] = request.GET['fb_sig_session_key'] + request.facebook.uid = request.session['fb_sig_user'] = request.GET['fb_sig_user'] + elif request.session.get('facebook_session_key', None) and request.session.get('facebook_user_id', None): + request.facebook.session_key = request.session['facebook_session_key'] + request.facebook.uid = request.session['facebook_user_id'] + + def process_response(self, request, response): + if not self.internal and request.facebook.session_key and request.facebook.uid: + request.session['facebook_session_key'] = request.facebook.session_key + request.session['facebook_user_id'] = request.facebook.uid + + if request.facebook.session_key_expires: + expiry = datetime.datetime.fromtimestamp(request.facebook.session_key_expires) + request.session.set_expiry(expiry) + + try: + fb = request.facebook + except: + return response + + if not fb.is_session_from_cookie: + # Make sure the browser accepts our session cookies inside an Iframe + response['P3P'] = 'CP="NOI DSP COR NID ADMa OPTa OUR NOR"' + fb_cookies = { + 'expires': fb.session_key_expires, + 'session_key': fb.session_key, + 'user': fb.uid, + } + + expire_time = None + if fb.session_key_expires: + expire_time = datetime.utcfromtimestamp(fb.session_key_expires) + + for k in fb_cookies: + response.set_cookie(self.api_key + '_' + k, fb_cookies[k], expires=expire_time) + response.set_cookie(self.api_key , fb._hash_args(fb_cookies), expires=expire_time) + + return response diff --git a/auth/auth_systems/facebookclient/djangofb/context_processors.py b/auth/auth_systems/facebookclient/djangofb/context_processors.py new file mode 100644 index 0000000000000000000000000000000000000000..6f954397308f7af525d9fa600fb29e8cf6902c33 --- /dev/null +++ b/auth/auth_systems/facebookclient/djangofb/context_processors.py @@ -0,0 +1,6 @@ +def messages(request): + """Returns messages similar to ``django.core.context_processors.auth``.""" + if hasattr(request, 'facebook') and request.facebook.uid is not None: + from models import Message + messages = Message.objects.get_and_delete_all(uid=request.facebook.uid) + return {'messages': messages} \ No newline at end of file diff --git a/auth/auth_systems/facebookclient/djangofb/default_app/__init__.py b/auth/auth_systems/facebookclient/djangofb/default_app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/auth/auth_systems/facebookclient/djangofb/default_app/models.py b/auth/auth_systems/facebookclient/djangofb/default_app/models.py new file mode 100644 index 0000000000000000000000000000000000000000..666ccd3f39403df207fac99cee88c6ca00789b8f --- /dev/null +++ b/auth/auth_systems/facebookclient/djangofb/default_app/models.py @@ -0,0 +1,31 @@ +from django.db import models + +# get_facebook_client lets us get the current Facebook object +# from outside of a view, which lets us have cleaner code +from facebook.djangofb import get_facebook_client + +class UserManager(models.Manager): + """Custom manager for a Facebook User.""" + + def get_current(self): + """Gets a User object for the logged-in Facebook user.""" + facebook = get_facebook_client() + user, created = self.get_or_create(id=int(facebook.uid)) + if created: + # we could do some custom actions for new users here... + pass + return user + +class User(models.Model): + """A simple User model for Facebook users.""" + + # We use the user's UID as the primary key in our database. + id = models.IntegerField(primary_key=True) + + # TODO: The data that you want to store for each user would go here. + # For this sample, we let users let people know their favorite progamming + # language, in the spirit of Extended Info. + language = models.CharField(maxlength=64, default='Python') + + # Add the custom manager + objects = UserManager() diff --git a/auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml b/auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml new file mode 100644 index 0000000000000000000000000000000000000000..6734dd17caa138540fb10d0fcb750d70c8600d33 --- /dev/null +++ b/auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml @@ -0,0 +1,22 @@ +<fb:header> + {% comment %} + We can use {{ fbuser }} to get at the current user. + {{ fbuser.id }} will be the user's UID, and {{ fbuser.language }} + is his/her favorite language (Python :-). + {% endcomment %} + Welcome, <fb:name uid="{{ fbuser.id }}" firstnameonly="true" useyou="false" />! +</fb:header> + +<div class="clearfix" style="float: left; border: 1px #d8dfea solid; padding: 10px 10px 10px 10px; margin-left: 30px; margin-bottom: 30px; width: 500px;"> + Your favorite language is {{ fbuser.language|escape }}. + <br /><br /> + + <div class="grayheader clearfix"> + <br /><br /> + + <form action="." method="POST"> + <input type="text" name="language" value="{{ fbuser.language|escape }}" /> + <input type="submit" value="Change" /> + </form> + </div> +</div> diff --git a/auth/auth_systems/facebookclient/djangofb/default_app/urls.py b/auth/auth_systems/facebookclient/djangofb/default_app/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..850184440c5fc153df12bf71e792cdccfe9baa57 --- /dev/null +++ b/auth/auth_systems/facebookclient/djangofb/default_app/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('{{ project }}.{{ app }}.views', + (r'^$', 'canvas'), + # Define other pages you want to create here +) + diff --git a/auth/auth_systems/facebookclient/djangofb/default_app/views.py b/auth/auth_systems/facebookclient/djangofb/default_app/views.py new file mode 100644 index 0000000000000000000000000000000000000000..931d6216a8f689167f7c5d22facf9b69551daeff --- /dev/null +++ b/auth/auth_systems/facebookclient/djangofb/default_app/views.py @@ -0,0 +1,37 @@ +from django.http import HttpResponse +from django.views.generic.simple import direct_to_template +#uncomment the following two lines and the one below +#if you dont want to use a decorator instead of the middleware +#from django.utils.decorators import decorator_from_middleware +#from facebook.djangofb import FacebookMiddleware + +# Import the Django helpers +import facebook.djangofb as facebook + +# The User model defined in models.py +from models import User + +# We'll require login for our canvas page. This +# isn't necessarily a good idea, as we might want +# to let users see the page without granting our app +# access to their info. See the wiki for details on how +# to do this. +#@decorator_from_middleware(FacebookMiddleware) +@facebook.require_login() +def canvas(request): + # Get the User object for the currently logged in user + user = User.objects.get_current() + + # Check if we were POSTed the user's new language of choice + if 'language' in request.POST: + user.language = request.POST['language'][:64] + user.save() + + # User is guaranteed to be logged in, so pass canvas.fbml + # an extra 'fbuser' parameter that is the User object for + # the currently logged in user. + return direct_to_template(request, 'canvas.fbml', extra_context={'fbuser': user}) + +@facebook.require_login() +def ajax(request): + return HttpResponse('hello world') diff --git a/auth/auth_systems/facebookclient/djangofb/models.py b/auth/auth_systems/facebookclient/djangofb/models.py new file mode 100644 index 0000000000000000000000000000000000000000..b5d2c62221e9926f7ab4b57cb95fb71ab22be2da --- /dev/null +++ b/auth/auth_systems/facebookclient/djangofb/models.py @@ -0,0 +1,36 @@ +from django.db import models +from django.utils.html import escape +from django.utils.safestring import mark_safe + +FB_MESSAGE_STATUS = ( + (0, 'Explanation'), + (1, 'Error'), + (2, 'Success'), +) + +class MessageManager(models.Manager): + def get_and_delete_all(self, uid): + messages = [] + for m in self.filter(uid=uid): + messages.append(m) + m.delete() + return messages + +class Message(models.Model): + """Represents a message for a Facebook user.""" + uid = models.CharField(max_length=25) + status = models.IntegerField(choices=FB_MESSAGE_STATUS) + message = models.CharField(max_length=300) + objects = MessageManager() + + def __unicode__(self): + return self.message + + def _fb_tag(self): + return self.get_status_display().lower() + + def as_fbml(self): + return mark_safe(u'<fb:%s message="%s" />' % ( + self._fb_tag(), + escape(self.message), + )) diff --git a/auth/auth_systems/facebookclient/webappfb.py b/auth/auth_systems/facebookclient/webappfb.py new file mode 100644 index 0000000000000000000000000000000000000000..5fdf77af5c05ce29ccd56b6326ca4b8f64a08294 --- /dev/null +++ b/auth/auth_systems/facebookclient/webappfb.py @@ -0,0 +1,170 @@ +# +# webappfb - Facebook tools for Google's AppEngine "webapp" Framework +# +# Copyright (c) 2009, Max Battcher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author nor the names of its contributors may +# be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from google.appengine.api import memcache +from google.appengine.ext.webapp import RequestHandler +from facebook import Facebook +import yaml + +""" +Facebook tools for Google AppEngine's object-oriented "webapp" framework. +""" + +# This global configuration dictionary is for configuration variables +# for Facebook requests such as the application's API key and secret +# key. Defaults to loading a 'facebook.yaml' YAML file. This should be +# useful and familiar for most AppEngine development. +FACEBOOK_CONFIG = yaml.load(file('facebook.yaml', 'r')) + +class FacebookRequestHandler(RequestHandler): + """ + Base class for request handlers for Facebook apps, providing useful + Facebook-related tools: a local + """ + + def _fbconfig_value(self, name, default=None): + """ + Checks the global config dictionary and then for a class/instance + variable, using a provided default if no value is found. + """ + if name in FACEBOOK_CONFIG: + default = FACEBOOK_CONFIG[name] + + return getattr(self, name, default) + + def initialize(self, request, response): + """ + Initialize's this request's Facebook client. + """ + super(FacebookRequestHandler, self).initialize(request, response) + + app_name = self._fbconfig_value('app_name', '') + api_key = self._fbconfig_value('api_key', None) + secret_key = self._fbconfig_value('secret_key', None) + + self.facebook = Facebook(api_key, secret_key, + app_name=app_name) + + require_app = self._fbconfig_value('require_app', False) + require_login = self._fbconfig_value('require_login', False) + need_session = self._fbconfig_value('need_session', False) + check_session = self._fbconfig_value('check_session', True) + + self._messages = None + self.redirecting = False + + if require_app or require_login: + if not self.facebook.check_session(request): + self.redirect(self.facebook.get_login_url(next=request.path)) + self.redirecting = True + return + elif check_session: + self.facebook.check_session(request) # ignore response + + # NOTE: require_app is deprecated according to modern Facebook login + # policies. Included for completeness, but unnecessary. + if require_app and not self.facebook.added: + self.redirect(self.facebook.get_add_url(next=request.path)) + self.redirecting = True + return + + if not (require_app or require_login) and need_session: + self.facebook.auth.getSession() + + def redirect(self, url, **kwargs): + """ + For Facebook canvas pages we should use <fb:redirect /> instead of + a normal redirect. + """ + if self.facebook.in_canvas: + self.response.clear() + self.response.out.write('<fb:redirect url="%s" />' % (url, )) + else: + super(FacebookRequestHandler, self).redirect(url, **kwargs) + + def add_user_message(self, kind, msg, detail='', time=15 * 60): + """ + Add a message to the current user to memcache. + """ + if self.facebook.uid: + key = 'messages:%s' % self.facebook.uid + self._messages = memcache.get(key) + message = { + 'kind': kind, + 'message': msg, + 'detail': detail, + } + if self._messages is not None: + self._messages.append(message) + else: + self._messages = [message] + memcache.set(key, self._messages, time=time) + + def get_and_delete_user_messages(self): + """ + Get all of the messages for the current user; removing them. + """ + if self.facebook.uid: + key = 'messages:%s' % self.facebook.uid + if not hasattr(self, '_messages') or self._messages is None: + self._messages = memcache.get(key) + memcache.delete(key) + return self._messages + return None + +class FacebookCanvasHandler(FacebookRequestHandler): + """ + Request handler for Facebook canvas (FBML application) requests. + """ + + def canvas(self, *args, **kwargs): + """ + This will be your handler to deal with Canvas requests. + """ + raise NotImplementedError() + + def get(self, *args): + """ + All valid canvas views are POSTS. + """ + # TODO: Attempt to auto-redirect to Facebook canvas? + self.error(404) + + def post(self, *args, **kwargs): + """ + Check a couple of simple safety checks and then call the canvas + handler. + """ + if self.redirecting: return + + if not self.facebook.in_canvas: + self.error(404) + return + + self.canvas(*args, **kwargs) + +# vim: ai et ts=4 sts=4 sw=4 diff --git a/auth/auth_systems/facebookclient/wsgi.py b/auth/auth_systems/facebookclient/wsgi.py new file mode 100644 index 0000000000000000000000000000000000000000..f6a790db14858d762159478a4aa51e7323144b5d --- /dev/null +++ b/auth/auth_systems/facebookclient/wsgi.py @@ -0,0 +1,129 @@ +"""This is some simple helper code to bridge the Pylons / PyFacebook gap. + +There's some generic WSGI middleware, some Paste stuff, and some Pylons +stuff. Once you put FacebookWSGIMiddleware into your middleware stack, +you'll have access to ``environ["pyfacebook.facebook"]``, which is a +``facebook.Facebook`` object. If you're using Paste (which includes +Pylons users), you can also access this directly using the facebook +global in this module. + +""" + +# Be careful what you import. Don't expect everyone to have Pylons, +# Paste, etc. installed. Degrade gracefully. + +from facebook import Facebook + +__docformat__ = "restructuredtext" + + +# Setup Paste, if available. This needs to stay in the same module as +# FacebookWSGIMiddleware below. + +try: + from paste.registry import StackedObjectProxy + from webob.exc import _HTTPMove + from paste.util.quoting import strip_html, html_quote, no_quote +except ImportError: + pass +else: + facebook = StackedObjectProxy(name="PyFacebook Facebook Connection") + + + class CanvasRedirect(_HTTPMove): + + """This is for canvas redirects.""" + + title = "See Other" + code = 200 + template = '<fb:redirect url="%(location)s" />' + + def html(self, environ): + """ text/html representation of the exception """ + body = self.make_body(environ, self.template, html_quote, no_quote) + return body + +class FacebookWSGIMiddleware(object): + + """This is WSGI middleware for Facebook.""" + + def __init__(self, app, config, facebook_class=Facebook): + """Initialize the Facebook middleware. + + ``app`` + This is the WSGI application being wrapped. + + ``config`` + This is a dict containing the keys "pyfacebook.apikey" and + "pyfacebook.secret". + + ``facebook_class`` + If you want to subclass the Facebook class, you can pass in + your replacement here. Pylons users will want to use + PylonsFacebook. + + """ + self.app = app + self.config = config + self.facebook_class = facebook_class + + def __call__(self, environ, start_response): + config = self.config + real_facebook = self.facebook_class(config["pyfacebook.apikey"], + config["pyfacebook.secret"]) + registry = environ.get('paste.registry') + if registry: + registry.register(facebook, real_facebook) + environ['pyfacebook.facebook'] = real_facebook + return self.app(environ, start_response) + + +# The remainder is Pylons specific. + +try: + import pylons + from pylons.controllers.util import redirect_to as pylons_redirect_to + from routes import url_for +except ImportError: + pass +else: + + + class PylonsFacebook(Facebook): + + """Subclass Facebook to add Pylons goodies.""" + + def check_session(self, request=None): + """The request parameter is now optional.""" + if request is None: + request = pylons.request + return Facebook.check_session(self, request) + + # The Django request object is similar enough to the Paste + # request object that check_session and validate_signature + # should *just work*. + + def redirect_to(self, url): + """Wrap Pylons' redirect_to function so that it works in_canvas. + + By the way, this won't work until after you call + check_session(). + + """ + if self.in_canvas: + raise CanvasRedirect(url) + pylons_redirect_to(url) + + def apps_url_for(self, *args, **kargs): + """Like url_for, but starts with "http://apps.facebook.com".""" + return "http://apps.facebook.com" + url_for(*args, **kargs) + + + def create_pylons_facebook_middleware(app, config): + """This is a simple wrapper for FacebookWSGIMiddleware. + + It passes the correct facebook_class. + + """ + return FacebookWSGIMiddleware(app, config, + facebook_class=PylonsFacebook) diff --git a/auth/auth_systems/google.py b/auth/auth_systems/google.py new file mode 100644 index 0000000000000000000000000000000000000000..847530be56dcc644a8053607226c7b77c8139565 --- /dev/null +++ b/auth/auth_systems/google.py @@ -0,0 +1,59 @@ +""" +Google Authentication + +""" + +from django.http import * +from django.core.mail import send_mail +from django.conf import settings + +import sys, os, cgi, urllib, urllib2, re +from xml.etree import ElementTree + +from openid import view_helpers + +# some parameters to indicate that status updating is not possible +STATUS_UPDATES = False + +# display tweaks +LOGIN_MESSAGE = "Log in with my Google Account" +OPENID_ENDPOINT = 'https://www.google.com/accounts/o8/id' + +# FIXME! +# TRUST_ROOT = 'http://localhost:8000' +# RETURN_TO = 'http://localhost:8000/auth/after' + +def get_auth_url(request, redirect_url): + # FIXME?? TRUST_ROOT should be diff than return_url? + request.session['google_redirect_url'] = redirect_url + url = view_helpers.start_openid(request.session, OPENID_ENDPOINT, redirect_url, redirect_url) + return url + +def get_user_info_after_auth(request): + data = view_helpers.finish_openid(request.session, request.GET, request.session['google_redirect_url']) + + return {'type' : 'google', 'user_id': data['ax']['email'][0], 'name': "%s %s" % (data['ax']['firstname'][0], data['ax']['lastname'][0]), 'info': {}, 'token':{}} + +def do_logout(user): + """ + logout of Google + """ + return None + +def update_status(token, message): + """ + simple update + """ + pass + +def send_message(user_id, name, user_info, subject, body): + """ + send email to google users. user_id is the email for google. + """ + send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (name, user_id)], fail_silently=False) + +def check_constraint(constraint, user_info): + """ + for eligibility + """ + pass diff --git a/auth/auth_systems/live.py b/auth/auth_systems/live.py new file mode 100644 index 0000000000000000000000000000000000000000..21eaf7c6dce0fa264c5fb41c6d156f537ef73eca --- /dev/null +++ b/auth/auth_systems/live.py @@ -0,0 +1,67 @@ +""" +Windows Live Authentication using oAuth WRAP, +so much like Facebook + +# NOT WORKING YET because Windows Live documentation and status is unclear. Is it in beta? I think it is. +""" + +import logging + +from django.conf import settings +APP_ID = settings.LIVE_APP_ID +APP_SECRET = settings.LIVE_APP_SECRET + +import urllib, urllib2, cgi + +# some parameters to indicate that status updating is possible +STATUS_UPDATES = False +# STATUS_UPDATE_WORDING_TEMPLATE = "Send %s to your facebook status" + +from auth import utils + +def live_url(url, params): + if params: + return "https://graph.facebook.com%s?%s" % (url, urllib.urlencode(params)) + else: + return "https://graph.facebook.com%s" % url + +def live_get(url, params): + full_url = live_url(url,params) + return urllib2.urlopen(full_url).read() + +def live_post(url, params): + full_url = live_url(url, None) + return urllib2.urlopen(full_url, urllib.urlencode(params)).read() + +def get_auth_url(request, redirect_url): + request.session['live_redirect_uri'] = redirect_url + return live_url('/oauth/authorize', { + 'client_id': APP_ID, + 'redirect_uri': redirect_url, + 'scope': 'publish_stream'}) + +def get_user_info_after_auth(request): + args = facebook_get('/oauth/access_token', { + 'client_id' : APP_ID, + 'redirect_uri' : request.session['fb_redirect_uri'], + 'client_secret' : API_SECRET, + 'code' : request.GET['code'] + }) + + access_token = cgi.parse_qs(args)['access_token'][0] + + info = utils.from_json(facebook_get('/me', {'access_token':access_token})) + + return {'type': 'facebook', 'user_id' : info['id'], 'name': info['name'], 'info': info, 'token': {'access_token': access_token}} + +def update_status(user_id, user_info, token, message): + """ + post a message to the auth system's update stream, e.g. twitter stream + """ + result = facebook_post('/me/feed', { + 'access_token': token['access_token'], + 'message': message + }) + +def send_message(user_id, user_name, user_info, subject, body): + pass diff --git a/auth/auth_systems/oauthclient/README b/auth/auth_systems/oauthclient/README new file mode 100644 index 0000000000000000000000000000000000000000..10deb937724ed4b3e9cded775b224739ab005fce --- /dev/null +++ b/auth/auth_systems/oauthclient/README @@ -0,0 +1,56 @@ +Python Oauth client for Twitter +--------- + +I built this so that i didn't have to keep looking for an oauth client for twitter to use in python. + +It is based off of the PHP work from abrah.am (http://github.com/poseurtech/twitteroauth/tree/master). +It was very helpful. + +I am using the OAuth lib that is from google gdata. I figure it is a working client and is in production use - so it should be solid. You can find it at: +http://gdata-python-client.googlecode.com/svn/trunk/src/gdata/oauth + +With a bit of modification this client should work with other publishers. + +btw, i am a python n00b. so feel free to help out. + +Thanks, +harper - harper@nata2.org (email and xmpp) + + +----------- +Links: + +Google Code Project: http://code.google.com/p/twitteroauth-python/ +Issue Tracker: http://code.google.com/p/twitteroauth-python/issues/list +Wiki: http://wiki.github.com/harperreed/twitteroauth-python + +----------- + +The example client is included in the client.py. It is: + +if __name__ == '__main__': + consumer_key = '' + consumer_secret = '' + while not consumer_key: + consumer_key = raw_input('Please enter consumer key: ') + while not consumer_secret: + consumer_secret = raw_input('Please enter consumer secret: ') + auth_client = TwitterOAuthClient(consumer_key,consumer_secret) + tok = auth_client.get_request_token() + token = tok['oauth_token'] + token_secret = tok['oauth_token_secret'] + url = auth_client.get_authorize_url(token) + webbrowser.open(url) + print "Visit this URL to authorize your app: " + url + response_token = raw_input('What is the oauth_token from twitter: ') + response_client = TwitterOAuthClient(consumer_key, consumer_secret,token, token_secret) + tok = response_client.get_access_token() + print "Making signed request" + #verify user access + content = response_client.oauth_request('https://twitter.com/account/verify_credentials.json', method='POST') + #make an update + #content = response_client.oauth_request('https://twitter.com/statuses/update.xml', {'status':'Updated from a python oauth client. awesome.'}, method='POST') + print content + + print 'Done.' + diff --git a/auth/auth_systems/oauthclient/__init__.py b/auth/auth_systems/oauthclient/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/auth/auth_systems/oauthclient/client.py b/auth/auth_systems/oauthclient/client.py new file mode 100644 index 0000000000000000000000000000000000000000..e1a4e18b7ccc2c84d6cccec0f0ab21f27ba120ad --- /dev/null +++ b/auth/auth_systems/oauthclient/client.py @@ -0,0 +1,147 @@ +''' +Python Oauth client for Twitter + +Used the SampleClient from the OAUTH.org example python client as basis. + +props to leahculver for making a very hard to use but in the end usable oauth lib. + +''' +import httplib +import urllib, urllib2 +import time +import webbrowser +import oauth as oauth +from urlparse import urlparse + +class TwitterOAuthClient(oauth.OAuthClient): + api_root_url = 'https://twitter.com' #for testing 'http://term.ie' + api_root_port = "80" + + #set api urls + def request_token_url(self): + return self.api_root_url + '/oauth/request_token' + def authorize_url(self): + return self.api_root_url + '/oauth/authorize' + def authenticate_url(self): + return self.api_root_url + '/oauth/authenticate' + def access_token_url(self): + return self.api_root_url + '/oauth/access_token' + + #oauth object + def __init__(self, consumer_key, consumer_secret, oauth_token=None, oauth_token_secret=None): + self.sha1_method = oauth.OAuthSignatureMethod_HMAC_SHA1() + self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret) + if ((oauth_token != None) and (oauth_token_secret!=None)): + self.token = oauth.OAuthConsumer(oauth_token, oauth_token_secret) + else: + self.token = None + + def oauth_request(self,url, args = {}, method=None): + if (method==None): + if args=={}: + method = "GET" + else: + method = "POST" + req = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token, method, url, args) + req.sign_request(self.sha1_method, self.consumer,self.token) + if (method=="GET"): + return self.http_wrapper(req.to_url()) + elif (method == "POST"): + return self.http_wrapper(req.get_normalized_http_url(),req.to_postdata()) + + #this is barely working. (i think. mostly it is that everyone else is using httplib) + def http_wrapper(self, url, postdata={}): + try: + if (postdata != {}): + f = urllib.urlopen(url, postdata) + else: + f = urllib.urlopen(url) + response = f.read() + except: + import traceback + import logging, sys + cla, exc, tb = sys.exc_info() + logging.error(url) + if postdata: + logging.error("with post data") + else: + logging.error("without post data") + logging.error(exc.args) + logging.error(traceback.format_tb(tb)) + response = "" + return response + + + def get_request_token(self): + response = self.oauth_request(self.request_token_url()) + token = self.oauth_parse_response(response) + try: + self.token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) + return token + except: + raise oauth.OAuthError('Invalid oauth_token') + + def oauth_parse_response(self, response_string): + r = {} + for param in response_string.split("&"): + pair = param.split("=") + if (len(pair)!=2): + break + + r[pair[0]]=pair[1] + return r + + def get_authorize_url(self, token): + return self.authorize_url() + '?oauth_token=' +token + + def get_authenticate_url(self, token): + return self.authenticate_url() + '?oauth_token=' +token + + def get_access_token(self,token=None): + r = self.oauth_request(self.access_token_url()) + token = self.oauth_parse_response(r) + self.token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) + return token + + def oauth_request(self, url, args={}, method=None): + if (method==None): + if args=={}: + method = "GET" + else: + method = "POST" + req = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token, method, url, args) + req.sign_request(self.sha1_method, self.consumer,self.token) + if (method=="GET"): + return self.http_wrapper(req.to_url()) + elif (method == "POST"): + return self.http_wrapper(req.get_normalized_http_url(),req.to_postdata()) + + + +if __name__ == '__main__': + consumer_key = '' + consumer_secret = '' + while not consumer_key: + consumer_key = raw_input('Please enter consumer key: ') + while not consumer_secret: + consumer_secret = raw_input('Please enter consumer secret: ') + auth_client = TwitterOAuthClient(consumer_key,consumer_secret) + tok = auth_client.get_request_token() + token = tok['oauth_token'] + token_secret = tok['oauth_token_secret'] + url = auth_client.get_authorize_url(token) + webbrowser.open(url) + print "Visit this URL to authorize your app: " + url + response_token = raw_input('What is the oauth_token from twitter: ') + response_client = TwitterOAuthClient(consumer_key, consumer_secret,token, token_secret) + tok = response_client.get_access_token() + print "Making signed request" + #verify user access + content = response_client.oauth_request('https://twitter.com/account/verify_credentials.json', method='POST') + #make an update + #content = response_client.oauth_request('https://twitter.com/statuses/update.xml', {'status':'Updated from a python oauth client. awesome.'}, method='POST') + print content + + print 'Done.' + + diff --git a/auth/auth_systems/oauthclient/oauth/CHANGES.txt b/auth/auth_systems/oauthclient/oauth/CHANGES.txt new file mode 100755 index 0000000000000000000000000000000000000000..7c2b92cd943df997919904c965f480268e47b701 --- /dev/null +++ b/auth/auth_systems/oauthclient/oauth/CHANGES.txt @@ -0,0 +1,17 @@ +1. Moved oauth.py to __init__.py + +2. Refactored __init__.py for compatibility with python 2.2 (Issue 59) + +3. Refactored rsa.py for compatibility with python 2.2 (Issue 59) + +4. Refactored OAuthRequest.from_token_and_callback since the callback url was +getting double url-encoding the callback url in place of single. (Issue 43) + +5. Added build_signature_base_string method to rsa.py since it used the +implementation of this method from oauth.OAuthSignatureMethod_HMAC_SHA1 which +was incorrect since it enforced the presence of a consumer secret and a token +secret. Also, changed its super class from oauth.OAuthSignatureMethod_HMAC_SHA1 +to oauth.OAuthSignatureMethod (Issue 64) + +6. Refactored <OAuthRequest>.to_header method since it returned non-oauth params +as well which was incorrect. (Issue 31) \ No newline at end of file diff --git a/auth/auth_systems/oauthclient/oauth/__init__.py b/auth/auth_systems/oauthclient/oauth/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..baf543ed4a8db09d92ee91b925d18bc6f5d09f93 --- /dev/null +++ b/auth/auth_systems/oauthclient/oauth/__init__.py @@ -0,0 +1,524 @@ +import cgi +import urllib +import time +import random +import urlparse +import hmac +import binascii + +VERSION = '1.0' # Hi Blaine! +HTTP_METHOD = 'GET' +SIGNATURE_METHOD = 'PLAINTEXT' + +# Generic exception class +class OAuthError(RuntimeError): + def __init__(self, message='OAuth error occured.'): + self.message = message + +# optional WWW-Authenticate header (401 error) +def build_authenticate_header(realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + +# url escape +def escape(s): + # escape '/' too + return urllib.quote(s, safe='~') + +# util function: current timestamp +# seconds since epoch (UTC) +def generate_timestamp(): + return int(time.time()) + +# util function: nonce +# pseudorandom number +def generate_nonce(length=8): + return ''.join([str(random.randint(0, 9)) for i in range(length)]) + +# OAuthConsumer is a data type that represents the identity of the Consumer +# via its shared secret with the Service Provider. +class OAuthConsumer(object): + key = None + secret = None + + def __init__(self, key, secret): + self.key = key + self.secret = secret + +# OAuthToken is a data type that represents an End User via either an access +# or request token. +class OAuthToken(object): + # access tokens and request tokens + key = None + secret = None + + ''' + key = the token + secret = the token secret + ''' + def __init__(self, key, secret): + self.key = key + self.secret = secret + + def to_string(self): + return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) + + # return a token from something like: + # oauth_token_secret=digg&oauth_token=digg + def from_string(s): + params = cgi.parse_qs(s, keep_blank_values=False) + key = params['oauth_token'][0] + secret = params['oauth_token_secret'][0] + return OAuthToken(key, secret) + from_string = staticmethod(from_string) + + def __str__(self): + return self.to_string() + +# OAuthRequest represents the request and can be serialized +class OAuthRequest(object): + ''' + OAuth parameters: + - oauth_consumer_key + - oauth_token + - oauth_signature_method + - oauth_signature + - oauth_timestamp + - oauth_nonce + - oauth_version + ... any additional parameters, as defined by the Service Provider. + ''' + parameters = None # oauth parameters + http_method = HTTP_METHOD + http_url = None + version = VERSION + + def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): + self.http_method = http_method + self.http_url = http_url + self.parameters = parameters or {} + + def set_parameter(self, parameter, value): + self.parameters[parameter] = value + + def get_parameter(self, parameter): + try: + return self.parameters[parameter] + except: + raise OAuthError('Parameter not found: %s' % parameter) + + def _get_timestamp_nonce(self): + return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') + + # get any non-oauth parameters + def get_nonoauth_parameters(self): + parameters = {} + for k, v in self.parameters.iteritems(): + # ignore oauth parameters + if k.find('oauth_') < 0: + parameters[k] = v + return parameters + + # serialize as a header for an HTTPAuth request + def to_header(self, realm=''): + auth_header = 'OAuth realm="%s"' % realm + # add the oauth parameters + if self.parameters: + for k, v in self.parameters.iteritems(): + if k[:6] == 'oauth_': + auth_header += ', %s="%s"' % (k, escape(str(v))) + return {'Authorization': auth_header} + + # serialize as post data for a POST request + def to_postdata(self): + return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()]) + + # serialize as a url for a GET request + def to_url(self): + return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) + + # return a string that consists of all the parameters that need to be signed + def get_normalized_parameters(self): + params = self.parameters + try: + # exclude the signature if it exists + del params['oauth_signature'] + except: + pass + key_values = params.items() + # sort lexicographically, first after key, then after value + key_values.sort() + # combine key value pairs in string and escape + return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values]) + + # just uppercases the http method + def get_normalized_http_method(self): + return self.http_method.upper() + + # parses the url and rebuilds it to be scheme://host/path + def get_normalized_http_url(self): + parts = urlparse.urlparse(self.http_url) + url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path + return url_string + + # set the signature parameter to the result of build_signature + def sign_request(self, signature_method, consumer, token): + # set the signature method + self.set_parameter('oauth_signature_method', signature_method.get_name()) + # set the signature + self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) + + def build_signature(self, signature_method, consumer, token): + # call the build signature method within the signature method + return signature_method.build_signature(self, consumer, token) + + def from_request(http_method, http_url, headers=None, parameters=None, query_string=None): + # combine multiple parameter sources + if parameters is None: + parameters = {} + + # headers + if headers and 'Authorization' in headers: + auth_header = headers['Authorization'] + # check that the authorization header is OAuth + if auth_header.index('OAuth') > -1: + try: + # get the parameters from the header + header_params = OAuthRequest._split_header(auth_header) + parameters.update(header_params) + except: + raise OAuthError('Unable to parse OAuth parameters from Authorization header.') + + # GET or POST query string + if query_string: + query_params = OAuthRequest._split_url_string(query_string) + parameters.update(query_params) + + # URL parameters + param_str = urlparse.urlparse(http_url)[4] # query + url_params = OAuthRequest._split_url_string(param_str) + parameters.update(url_params) + + if parameters: + return OAuthRequest(http_method, http_url, parameters) + + return None + from_request = staticmethod(from_request) + + def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + defaults = { + 'oauth_consumer_key': oauth_consumer.key, + 'oauth_timestamp': generate_timestamp(), + 'oauth_nonce': generate_nonce(), + 'oauth_version': OAuthRequest.version, + } + + defaults.update(parameters) + parameters = defaults + + if token: + parameters['oauth_token'] = token.key + + return OAuthRequest(http_method, http_url, parameters) + from_consumer_and_token = staticmethod(from_consumer_and_token) + + def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + parameters['oauth_token'] = token.key + + if callback: + parameters['oauth_callback'] = callback + + return OAuthRequest(http_method, http_url, parameters) + from_token_and_callback = staticmethod(from_token_and_callback) + + # util function: turn Authorization: header into parameters, has to do some unescaping + def _split_header(header): + params = {} + parts = header.split(',') + for param in parts: + # ignore realm parameter + if param.find('OAuth realm') > -1: + continue + # remove whitespace + param = param.strip() + # split key-value + param_parts = param.split('=', 1) + # remove quotes and unescape the value + params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) + return params + _split_header = staticmethod(_split_header) + + # util function: turn url string into parameters, has to do some unescaping + def _split_url_string(param_str): + parameters = cgi.parse_qs(param_str, keep_blank_values=False) + for k, v in parameters.iteritems(): + parameters[k] = urllib.unquote(v[0]) + return parameters + _split_url_string = staticmethod(_split_url_string) + +# OAuthServer is a worker to check a requests validity against a data store +class OAuthServer(object): + timestamp_threshold = 300 # in seconds, five minutes + version = VERSION + signature_methods = None + data_store = None + + def __init__(self, data_store=None, signature_methods=None): + self.data_store = data_store + self.signature_methods = signature_methods or {} + + def set_data_store(self, oauth_data_store): + self.data_store = data_store + + def get_data_store(self): + return self.data_store + + def add_signature_method(self, signature_method): + self.signature_methods[signature_method.get_name()] = signature_method + return self.signature_methods + + # process a request_token request + # returns the request token on success + def fetch_request_token(self, oauth_request): + try: + # get the request token for authorization + token = self._get_token(oauth_request, 'request') + except OAuthError: + # no token required for the initial token request + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + self._check_signature(oauth_request, consumer, None) + # fetch a new token + token = self.data_store.fetch_request_token(consumer) + return token + + # process an access_token request + # returns the access token on success + def fetch_access_token(self, oauth_request): + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the request token + token = self._get_token(oauth_request, 'request') + self._check_signature(oauth_request, consumer, token) + new_token = self.data_store.fetch_access_token(consumer, token) + return new_token + + # verify an api call, checks all the parameters + def verify_request(self, oauth_request): + # -> consumer and token + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the access token + token = self._get_token(oauth_request, 'access') + self._check_signature(oauth_request, consumer, token) + parameters = oauth_request.get_nonoauth_parameters() + return consumer, token, parameters + + # authorize a request token + def authorize_token(self, token, user): + return self.data_store.authorize_request_token(token, user) + + # get the callback url + def get_callback(self, oauth_request): + return oauth_request.get_parameter('oauth_callback') + + # optional support for the authenticate header + def build_authenticate_header(self, realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + + # verify the correct version request for this server + def _get_version(self, oauth_request): + try: + version = oauth_request.get_parameter('oauth_version') + except: + version = VERSION + if version and version != self.version: + raise OAuthError('OAuth version %s not supported.' % str(version)) + return version + + # figure out the signature with some defaults + def _get_signature_method(self, oauth_request): + try: + signature_method = oauth_request.get_parameter('oauth_signature_method') + except: + signature_method = SIGNATURE_METHOD + try: + # get the signature method object + signature_method = self.signature_methods[signature_method] + except: + signature_method_names = ', '.join(self.signature_methods.keys()) + raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) + + return signature_method + + def _get_consumer(self, oauth_request): + consumer_key = oauth_request.get_parameter('oauth_consumer_key') + if not consumer_key: + raise OAuthError('Invalid consumer key.') + consumer = self.data_store.lookup_consumer(consumer_key) + if not consumer: + raise OAuthError('Invalid consumer.') + return consumer + + # try to find the token for the provided request token key + def _get_token(self, oauth_request, token_type='access'): + token_field = oauth_request.get_parameter('oauth_token') + token = self.data_store.lookup_token(token_type, token_field) + if not token: + raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) + return token + + def _check_signature(self, oauth_request, consumer, token): + timestamp, nonce = oauth_request._get_timestamp_nonce() + self._check_timestamp(timestamp) + self._check_nonce(consumer, token, nonce) + signature_method = self._get_signature_method(oauth_request) + try: + signature = oauth_request.get_parameter('oauth_signature') + except: + raise OAuthError('Missing signature.') + # validate the signature + valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature) + if not valid_sig: + key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) + raise OAuthError('Invalid signature. Expected signature base string: %s' % base) + built = signature_method.build_signature(oauth_request, consumer, token) + + def _check_timestamp(self, timestamp): + # verify that timestamp is recentish + timestamp = int(timestamp) + now = int(time.time()) + lapsed = now - timestamp + if lapsed > self.timestamp_threshold: + raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) + + def _check_nonce(self, consumer, token, nonce): + # verify that the nonce is uniqueish + nonce = self.data_store.lookup_nonce(consumer, token, nonce) + if nonce: + raise OAuthError('Nonce already used: %s' % str(nonce)) + +# OAuthClient is a worker to attempt to execute a request +class OAuthClient(object): + consumer = None + token = None + + def __init__(self, oauth_consumer, oauth_token): + self.consumer = oauth_consumer + self.token = oauth_token + + def get_consumer(self): + return self.consumer + + def get_token(self): + return self.token + + def fetch_request_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def access_resource(self, oauth_request): + # -> some protected resource + raise NotImplementedError + +# OAuthDataStore is a database abstraction used to lookup consumers and tokens +class OAuthDataStore(object): + + def lookup_consumer(self, key): + # -> OAuthConsumer + raise NotImplementedError + + def lookup_token(self, oauth_consumer, token_type, token_token): + # -> OAuthToken + raise NotImplementedError + + def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): + # -> OAuthToken + raise NotImplementedError + + def fetch_request_token(self, oauth_consumer): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_consumer, oauth_token): + # -> OAuthToken + raise NotImplementedError + + def authorize_request_token(self, oauth_token, user): + # -> OAuthToken + raise NotImplementedError + +# OAuthSignatureMethod is a strategy class that implements a signature method +class OAuthSignatureMethod(object): + def get_name(self): + # -> str + raise NotImplementedError + + def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): + # -> str key, str raw + raise NotImplementedError + + def build_signature(self, oauth_request, oauth_consumer, oauth_token): + # -> str + raise NotImplementedError + + def check_signature(self, oauth_request, consumer, token, signature): + built = self.build_signature(oauth_request, consumer, token) + return built == signature + +class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): + + def get_name(self): + return 'HMAC-SHA1' + + def build_signature_base_string(self, oauth_request, consumer, token): + sig = ( + escape(oauth_request.get_normalized_http_method()), + escape(oauth_request.get_normalized_http_url()), + escape(oauth_request.get_normalized_parameters()), + ) + + key = '%s&' % escape(consumer.secret) + if token: + key += escape(token.secret) + raw = '&'.join(sig) + return key, raw + + def build_signature(self, oauth_request, consumer, token): + # build the base signature string + key, raw = self.build_signature_base_string(oauth_request, consumer, token) + + # hmac object + try: + import hashlib # 2.5 + hashed = hmac.new(key, raw, hashlib.sha1) + except: + import sha # deprecated + hashed = hmac.new(key, raw, sha) + + # calculate the digest base 64 + return binascii.b2a_base64(hashed.digest())[:-1] + +class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): + + def get_name(self): + return 'PLAINTEXT' + + def build_signature_base_string(self, oauth_request, consumer, token): + # concatenate the consumer key and secret + sig = escape(consumer.secret) + '&' + if token: + sig = sig + escape(token.secret) + return sig + + def build_signature(self, oauth_request, consumer, token): + return self.build_signature_base_string(oauth_request, consumer, token) diff --git a/auth/auth_systems/oauthclient/oauth/rsa.py b/auth/auth_systems/oauthclient/oauth/rsa.py new file mode 100755 index 0000000000000000000000000000000000000000..f8d9b8503f7a35aa57cfef1fe75445baabae596a --- /dev/null +++ b/auth/auth_systems/oauthclient/oauth/rsa.py @@ -0,0 +1,120 @@ +#!/usr/bin/python + +""" +requires tlslite - http://trevp.net/tlslite/ + +""" + +import binascii + +from gdata.tlslite.utils import keyfactory +from gdata.tlslite.utils import cryptomath + +# XXX andy: ugly local import due to module name, oauth.oauth +import gdata.oauth as oauth + +class OAuthSignatureMethod_RSA_SHA1(oauth.OAuthSignatureMethod): + def get_name(self): + return "RSA-SHA1" + + def _fetch_public_cert(self, oauth_request): + # not implemented yet, ideas are: + # (1) do a lookup in a table of trusted certs keyed off of consumer + # (2) fetch via http using a url provided by the requester + # (3) some sort of specific discovery code based on request + # + # either way should return a string representation of the certificate + raise NotImplementedError + + def _fetch_private_cert(self, oauth_request): + # not implemented yet, ideas are: + # (1) do a lookup in a table of trusted certs keyed off of consumer + # + # either way should return a string representation of the certificate + raise NotImplementedError + + def build_signature_base_string(self, oauth_request, consumer, token): + sig = ( + oauth.escape(oauth_request.get_normalized_http_method()), + oauth.escape(oauth_request.get_normalized_http_url()), + oauth.escape(oauth_request.get_normalized_parameters()), + ) + key = '' + raw = '&'.join(sig) + return key, raw + + def build_signature(self, oauth_request, consumer, token): + key, base_string = self.build_signature_base_string(oauth_request, + consumer, + token) + + # Fetch the private key cert based on the request + cert = self._fetch_private_cert(oauth_request) + + # Pull the private key from the certificate + privatekey = keyfactory.parsePrivateKey(cert) + + # Convert base_string to bytes + #base_string_bytes = cryptomath.createByteArraySequence(base_string) + + # Sign using the key + signed = privatekey.hashAndSign(base_string) + + return binascii.b2a_base64(signed)[:-1] + + def check_signature(self, oauth_request, consumer, token, signature): + decoded_sig = base64.b64decode(signature); + + key, base_string = self.build_signature_base_string(oauth_request, + consumer, + token) + + # Fetch the public key cert based on the request + cert = self._fetch_public_cert(oauth_request) + + # Pull the public key from the certificate + publickey = keyfactory.parsePEMKey(cert, public=True) + + # Check the signature + ok = publickey.hashAndVerify(decoded_sig, base_string) + + return ok + + +class TestOAuthSignatureMethod_RSA_SHA1(OAuthSignatureMethod_RSA_SHA1): + def _fetch_public_cert(self, oauth_request): + cert = """ +-----BEGIN CERTIFICATE----- +MIIBpjCCAQ+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAZMRcwFQYDVQQDDA5UZXN0 +IFByaW5jaXBhbDAeFw03MDAxMDEwODAwMDBaFw0zODEyMzEwODAwMDBaMBkxFzAV +BgNVBAMMDlRlc3QgUHJpbmNpcGFsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQC0YjCwIfYoprq/FQO6lb3asXrxLlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlY +zypSRjVxwxrsuRcP3e641SdASwfrmzyvIgP08N4S0IFzEURkV1wp/IpH7kH41Etb +mUmrXSwfNZsnQRE5SYSOhh+LcK2wyQkdgcMv11l4KoBkcwIDAQABMA0GCSqGSIb3 +DQEBBQUAA4GBAGZLPEuJ5SiJ2ryq+CmEGOXfvlTtEL2nuGtr9PewxkgnOjZpUy+d +4TvuXJbNQc8f4AMWL/tO9w0Fk80rWKp9ea8/df4qMq5qlFWlx6yOLQxumNOmECKb +WpkUQDIDJEoFUzKMVuJf4KO/FJ345+BNLGgbJ6WujreoM1X/gYfdnJ/J +-----END CERTIFICATE----- +""" + return cert + + def _fetch_private_cert(self, oauth_request): + cert = """ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V +A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d +7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ +hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H +X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm +uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw +rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z +zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn +qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG +WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno +cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+ +3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8 +AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54 +Lw03eHTNQghS0A== +-----END PRIVATE KEY----- +""" + return cert diff --git a/auth/auth_systems/openid/__init__.py b/auth/auth_systems/openid/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d3a0cfc8d0a0a636124277f21d568061ecaf11ec --- /dev/null +++ b/auth/auth_systems/openid/__init__.py @@ -0,0 +1,7 @@ +""" +OpenID utils + +some copied from http://github.com/openid/python-openid/examples/djopenid + +ben@adida.net +""" diff --git a/auth/auth_systems/openid/util.py b/auth/auth_systems/openid/util.py new file mode 100644 index 0000000000000000000000000000000000000000..265cedb93f16a0298b42591d4b6655c28fbdccfb --- /dev/null +++ b/auth/auth_systems/openid/util.py @@ -0,0 +1,148 @@ + +""" +Utility code for the Django example consumer and server. +""" + +from urlparse import urljoin + +from django.db import connection +from django.template.context import RequestContext +from django.template import loader +from django import http +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse as reverseURL +from django.views.generic.simple import direct_to_template + +from django.conf import settings + +from openid.store.filestore import FileOpenIDStore +from openid.store import sqlstore +from openid.yadis.constants import YADIS_CONTENT_TYPE + +def getOpenIDStore(filestore_path, table_prefix): + """ + Returns an OpenID association store object based on the database + engine chosen for this Django application. + + * If no database engine is chosen, a filesystem-based store will + be used whose path is filestore_path. + + * If a database engine is chosen, a store object for that database + type will be returned. + + * If the chosen engine is not supported by the OpenID library, + raise ImproperlyConfigured. + + * If a database store is used, this will create the tables + necessary to use it. The table names will be prefixed with + table_prefix. DO NOT use the same table prefix for both an + OpenID consumer and an OpenID server in the same database. + + The result of this function should be passed to the Consumer + constructor as the store parameter. + """ + if not settings.DATABASE_ENGINE: + return FileOpenIDStore(filestore_path) + + # Possible side-effect: create a database connection if one isn't + # already open. + connection.cursor() + + # Create table names to specify for SQL-backed stores. + tablenames = { + 'associations_table': table_prefix + 'openid_associations', + 'nonces_table': table_prefix + 'openid_nonces', + } + + types = { + 'postgresql': sqlstore.PostgreSQLStore, + 'postgresql_psycopg2': sqlstore.PostgreSQLStore, + 'mysql': sqlstore.MySQLStore, + 'sqlite3': sqlstore.SQLiteStore, + } + + try: + s = types[settings.DATABASE_ENGINE](connection.connection, + **tablenames) + except KeyError: + raise ImproperlyConfigured, \ + "Database engine %s not supported by OpenID library" % \ + (settings.DATABASE_ENGINE,) + + try: + s.createTables() + except (SystemExit, KeyboardInterrupt, MemoryError), e: + raise + except: + # XXX This is not the Right Way to do this, but because the + # underlying database implementation might differ in behavior + # at this point, we can't reliably catch the right + # exception(s) here. Ideally, the SQL store in the OpenID + # library would catch exceptions that it expects and fail + # silently, but that could be bad, too. More ideally, the SQL + # store would not attempt to create tables it knows already + # exists. + pass + + return s + +def getViewURL(req, view_name_or_obj, args=None, kwargs=None): + relative_url = reverseURL(view_name_or_obj, args=args, kwargs=kwargs) + full_path = req.META.get('SCRIPT_NAME', '') + relative_url + return urljoin(getBaseURL(req), full_path) + +def getBaseURL(req): + """ + Given a Django web request object, returns the OpenID 'trust root' + for that request; namely, the absolute URL to the site root which + is serving the Django request. The trust root will include the + proper scheme and authority. It will lack a port if the port is + standard (80, 443). + """ + name = req.META['HTTP_HOST'] + try: + name = name[:name.index(':')] + except: + pass + + try: + port = int(req.META['SERVER_PORT']) + except: + port = 80 + + proto = req.META['SERVER_PROTOCOL'] + + if 'HTTPS' in proto: + proto = 'https' + else: + proto = 'http' + + if port in [80, 443] or not port: + port = '' + else: + port = ':%s' % (port,) + + url = "%s://%s%s/" % (proto, name, port) + return url + +def normalDict(request_data): + """ + Converts a django request MutliValueDict (e.g., request.GET, + request.POST) into a standard python dict whose values are the + first value from each of the MultiValueDict's value lists. This + avoids the OpenID library's refusal to deal with dicts whose + values are lists, because in OpenID, each key in the query arg set + can have at most one value. + """ + return dict((k, v[0]) for k, v in request_data.iteritems()) + +def renderXRDS(request, type_uris, endpoint_urls): + """Render an XRDS page with the specified type URIs and endpoint + URLs in one service block, and return a response with the + appropriate content-type. + """ + response = direct_to_template( + request, 'xrds.xml', + {'type_uris':type_uris, 'endpoint_urls':endpoint_urls,}) + response['Content-Type'] = YADIS_CONTENT_TYPE + return response diff --git a/auth/auth_systems/openid/view_helpers.py b/auth/auth_systems/openid/view_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..d5ce72cd81290c2541b5c1cd31f7b1f94da911a8 --- /dev/null +++ b/auth/auth_systems/openid/view_helpers.py @@ -0,0 +1,160 @@ + +from django import http +from django.http import HttpResponseRedirect +from django.views.generic.simple import direct_to_template + +from openid.consumer import consumer +from openid.consumer.discover import DiscoveryFailure +from openid.extensions import ax, pape, sreg +from openid.yadis.constants import YADIS_HEADER_NAME, YADIS_CONTENT_TYPE +from openid.server.trustroot import RP_RETURN_TO_URL_TYPE + +import util + +PAPE_POLICIES = [ + 'AUTH_PHISHING_RESISTANT', + 'AUTH_MULTI_FACTOR', + 'AUTH_MULTI_FACTOR_PHYSICAL', + ] + +AX_REQUIRED_FIELDS = { + 'firstname' : 'http://axschema.org/namePerson/first', + 'lastname' : 'http://axschema.org/namePerson/last', + 'fullname' : 'http://axschema.org/namePerson', + 'email' : 'http://axschema.org/contact/email' +} + +# List of (name, uri) for use in generating the request form. +POLICY_PAIRS = [(p, getattr(pape, p)) + for p in PAPE_POLICIES] + +def getOpenIDStore(): + """ + Return an OpenID store object fit for the currently-chosen + database backend, if any. + """ + return util.getOpenIDStore('/tmp/djopenid_c_store', 'c_') + +def get_consumer(session): + """ + Get a Consumer object to perform OpenID authentication. + """ + return consumer.Consumer(session, getOpenIDStore()) + +def start_openid(session, openid_url, trust_root, return_to): + """ + Start the OpenID authentication process. + + * Requests some Simple Registration data using the OpenID + library's Simple Registration machinery + + * Generates the appropriate trust root and return URL values for + this application (tweak where appropriate) + + * Generates the appropriate redirect based on the OpenID protocol + version. + """ + + # Start OpenID authentication. + c = get_consumer(session) + error = None + + try: + auth_request = c.begin(openid_url) + except DiscoveryFailure, e: + # Some other protocol-level failure occurred. + error = "OpenID discovery error: %s" % (str(e),) + + if error: + import pdb; pdb.set_trace() + raise Exception("error in openid") + + # Add Simple Registration request information. Some fields + # are optional, some are required. It's possible that the + # server doesn't support sreg or won't return any of the + # fields. + sreg_request = sreg.SRegRequest(required=['email'], + optional=[]) + auth_request.addExtension(sreg_request) + + # Add Attribute Exchange request information. + ax_request = ax.FetchRequest() + # XXX - uses myOpenID-compatible schema values, which are + # not those listed at axschema.org. + + for k, v in AX_REQUIRED_FIELDS.iteritems(): + ax_request.add(ax.AttrInfo(v, required=True)) + + auth_request.addExtension(ax_request) + + # Compute the trust root and return URL values to build the + # redirect information. + # trust_root = util.getViewURL(request, startOpenID) + # return_to = util.getViewURL(request, finishOpenID) + + # Send the browser to the server either by sending a redirect + # URL or by generating a POST form. + url = auth_request.redirectURL(trust_root, return_to) + return url + +def finish_openid(session, request_args, return_to): + """ + Finish the OpenID authentication process. Invoke the OpenID + library with the response from the OpenID server and render a page + detailing the result. + """ + result = {} + + # Because the object containing the query parameters is a + # MultiValueDict and the OpenID library doesn't allow that, we'll + # convert it to a normal dict. + + if request_args: + c = get_consumer(session) + + # Get a response object indicating the result of the OpenID + # protocol. + response = c.complete(request_args, return_to) + + # Get a Simple Registration response object if response + # information was included in the OpenID response. + sreg_response = {} + ax_items = {} + if response.status == consumer.SUCCESS: + sreg_response = sreg.SRegResponse.fromSuccessResponse(response) + + ax_response = ax.FetchResponse.fromSuccessResponse(response) + if ax_response: + for k, v in AX_REQUIRED_FIELDS.iteritems(): + """ + the values are the URIs, they are the key into the data + the key is the shortname + """ + if ax_response.data.has_key(v): + ax_items[k] = ax_response.get(v) + + # Map different consumer status codes to template contexts. + results = { + consumer.CANCEL: + {'message': 'OpenID authentication cancelled.'}, + + consumer.FAILURE: + {'error': 'OpenID authentication failed.'}, + + consumer.SUCCESS: + {'url': response.getDisplayIdentifier(), + 'sreg': sreg_response and sreg_response.items(), + 'ax': ax_items} + } + + result = results[response.status] + + if isinstance(response, consumer.FailureResponse): + # In a real application, this information should be + # written to a log for debugging/tracking OpenID + # authentication failures. In general, the messages are + # not user-friendly, but intended for developers. + result['failure_reason'] = response.message + + return result + diff --git a/auth/auth_systems/password.py b/auth/auth_systems/password.py new file mode 100644 index 0000000000000000000000000000000000000000..744012f4e49984241c28007c5edc7edfe9a12408 --- /dev/null +++ b/auth/auth_systems/password.py @@ -0,0 +1,116 @@ +""" +Username/Password Authentication +""" + +from django.core.urlresolvers import reverse +from django import forms +from django.core.mail import send_mail +from django.conf import settings +from django.http import HttpResponseRedirect + +import logging + +# some parameters to indicate that status updating is possible +STATUS_UPDATES = False + + +def create_user(username, password, name = None): + from auth.models import User + + user = User.get_by_type_and_id('password', username) + if user: + raise Exception('user exists') + + info = {'password' : password, 'name': name} + user = User.update_or_create(user_type='password', user_id=username, info = info) + user.save() + +class LoginForm(forms.Form): + username = forms.CharField(max_length=50) + password = forms.CharField(widget=forms.PasswordInput(), max_length=100) + +def password_check(user, password): + return (user and user.info['password'] == password) + +# the view for logging in +def password_login_view(request): + from auth.view_utils import render_template + from auth.views import after + from auth.models import User + + error = None + + if request.method == "GET": + form = LoginForm() + else: + form = LoginForm(request.POST) + + if form.is_valid(): + username = form.cleaned_data['username'].strip() + password = form.cleaned_data['password'].strip() + try: + user = User.get_by_type_and_id('password', username) + if password_check(user, password): + # set this in case we came here from another location than + # the normal login process + request.session['auth_system_name'] = 'password' + if request.POST.has_key('return_url'): + request.session['auth_return_url'] = request.POST.get('return_url') + + request.session['user'] = user + return HttpResponseRedirect(reverse(after)) + except User.DoesNotExist: + pass + error = 'Bad Username or Password' + + return render_template(request, 'password/login', {'form': form, 'error': error}) + +def password_forgotten_view(request): + """ + forgotten password view and submit. + includes return_url + """ + from auth.view_utils import render_template + from auth.models import User + + if request.method == "GET": + return render_template(request, 'password/forgot', {'return_url': request.GET.get('return_url', '')}) + else: + username = request.POST['username'] + return_url = request.POST['return_url'] + + user = User.get_by_type_and_id('password', username) + + body = """ + +This is a password reminder: + +Your username: %s +Your password: %s + +-- +%s +""" % (user.user_id, user.info['password'], settings.SITE_TITLE) + + # FIXME: make this a task + send_mail('password reminder', body, settings.SERVER_EMAIL, ["%s <%s>" % (user.info['name'], user.info['email'])], fail_silently=False) + + return HttpResponseRedirect(return_url) + +def get_auth_url(request, redirect_url = None): + return reverse(password_login_view) + +def get_user_info_after_auth(request): + user = request.session['user'] + user_info = user.info + + return {'type': 'password', 'user_id' : user.user_id, 'name': user.name, 'info': user.info, 'token': None} + +def update_status(token, message): + pass + +def send_message(user_id, user_name, user_info, subject, body): + if user_info.has_key('email'): + email = user_info['email'] + name = user_info.get('name', email) + send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (name, email)], fail_silently=False) diff --git a/auth/auth_systems/twitter.py b/auth/auth_systems/twitter.py new file mode 100644 index 0000000000000000000000000000000000000000..73f8bfe21b623025c89eeeebd1991d098b0d7ec6 --- /dev/null +++ b/auth/auth_systems/twitter.py @@ -0,0 +1,109 @@ +""" +Twitter Authentication +""" + +from oauthclient import client + +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect + +from auth import utils + +import logging + +from django.conf import settings +API_KEY = settings.TWITTER_API_KEY +API_SECRET = settings.TWITTER_API_SECRET +USER_TO_FOLLOW = settings.TWITTER_USER_TO_FOLLOW +REASON_TO_FOLLOW = settings.TWITTER_REASON_TO_FOLLOW +DM_TOKEN = settings.TWITTER_DM_TOKEN + +# some parameters to indicate that status updating is possible +STATUS_UPDATES = True +STATUS_UPDATE_WORDING_TEMPLATE = "Tweet %s" + +def _get_new_client(token=None, token_secret=None): + if token: + return client.TwitterOAuthClient(API_KEY, API_SECRET, token, token_secret) + else: + return client.TwitterOAuthClient(API_KEY, API_SECRET) + +def _get_client_by_token(token): + return _get_new_client(token['oauth_token'], token['oauth_token_secret']) + +def get_auth_url(request, redirect_url): + client = _get_new_client() + try: + tok = client.get_request_token() + except: + return None + + request.session['request_token'] = tok + url = client.get_authenticate_url(tok['oauth_token']) + return url + +def get_user_info_after_auth(request): + tok = request.session['request_token'] + twitter_client = _get_client_by_token(tok) + access_token = twitter_client.get_access_token() + request.session['access_token'] = access_token + + user_info = utils.from_json(twitter_client.oauth_request('http://api.twitter.com/1/account/verify_credentials.json', args={}, method='GET')) + + return {'type': 'twitter', 'user_id' : user_info['screen_name'], 'name': user_info['name'], 'info': user_info, 'token': access_token} + + +def user_needs_intervention(user_id, user_info, token): + """ + check to see if user is following the users we need + """ + twitter_client = _get_client_by_token(token) + friendship = utils.from_json(twitter_client.oauth_request('http://api.twitter.com/1/friendships/exists.json', args={'user_a': user_id, 'user_b': USER_TO_FOLLOW}, method='GET')) + if friendship: + return None + + return HttpResponseRedirect(reverse(follow_view)) + +def _get_client_by_request(request): + access_token = request.session['access_token'] + return _get_client_by_token(access_token) + +def update_status(user_id, user_info, token, message): + """ + post a message to the auth system's update stream, e.g. twitter stream + """ + twitter_client = _get_client_by_token(token) + result = twitter_client.oauth_request('http://api.twitter.com/1/statuses/update.json', args={'status': message}, method='POST') + +def send_message(user_id, user_name, user_info, subject, body): + pass + +def send_notification(user_id, user_info, message): + twitter_client = _get_client_by_token(DM_TOKEN) + result = twitter_client.oauth_request('http://api.twitter.com/1/direct_messages/new.json', args={'screen_name': user_id, 'text': message}, method='POST') + +## +## views +## + +def follow_view(request): + if request.method == "GET": + from auth.view_utils import render_template + from auth.views import after + + return render_template(request, 'twitter/follow', {'user_to_follow': USER_TO_FOLLOW, 'reason_to_follow' : REASON_TO_FOLLOW}) + + if request.method == "POST": + follow_p = bool(request.POST.get('follow_p',False)) + + if follow_p: + from auth.security import get_user + + user = get_user(request) + twitter_client = _get_client_by_token(user.token) + result = twitter_client.oauth_request('http://api.twitter.com/1/friendships/create.json', args={'screen_name': USER_TO_FOLLOW}, method='POST') + + from auth.views import after_intervention + return HttpResponseRedirect(reverse(after_intervention)) + + diff --git a/auth/auth_systems/yahoo.py b/auth/auth_systems/yahoo.py new file mode 100644 index 0000000000000000000000000000000000000000..48dd2464ddd7c0b57da09b839cd4983bd3c15cdc --- /dev/null +++ b/auth/auth_systems/yahoo.py @@ -0,0 +1,54 @@ +""" +Yahoo Authentication + +""" + +from django.http import * +from django.core.mail import send_mail +from django.conf import settings + +import sys, os, cgi, urllib, urllib2, re +from xml.etree import ElementTree + +from openid import view_helpers + +# some parameters to indicate that status updating is not possible +STATUS_UPDATES = False + +# display tweaks +LOGIN_MESSAGE = "Log in with my Yahoo Account" +OPENID_ENDPOINT = 'yahoo.com' + +def get_auth_url(request, redirect_url): + request.session['yahoo_redirect_url'] = redirect_url + url = view_helpers.start_openid(request.session, OPENID_ENDPOINT, redirect_url, redirect_url) + return url + +def get_user_info_after_auth(request): + data = view_helpers.finish_openid(request.session, request.GET, request.session['yahoo_redirect_url']) + + return {'type' : 'yahoo', 'user_id': data['ax']['email'][0], 'name': data['ax']['fullname'][0], 'info': {}, 'token':{}} + +def do_logout(user): + """ + logout of Yahoo + """ + return None + +def update_status(token, message): + """ + simple update + """ + pass + +def send_message(user_id, user_name, user_info, subject, body): + """ + send email to yahoo user, user_id is email for yahoo and other openID logins. + """ + send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (user_name, user_id)], fail_silently=False) + +def check_constraint(constraint, user_info): + """ + for eligibility + """ + pass diff --git a/auth/bbtests.py b/auth/bbtests.py new file mode 100644 index 0000000000000000000000000000000000000000..5c3da268257764389155da838d04506cc62282a1 --- /dev/null +++ b/auth/bbtests.py @@ -0,0 +1,5 @@ +""" +Black-box tests for Auth Systems + +2010-08-11 +""" diff --git a/auth/jsonfield.py b/auth/jsonfield.py new file mode 100644 index 0000000000000000000000000000000000000000..54311acd90e769779d42d589d5df18029bba9806 --- /dev/null +++ b/auth/jsonfield.py @@ -0,0 +1,55 @@ +""" +taken from + +http://www.djangosnippets.org/snippets/377/ +""" + +import datetime +from django.db import models +from django.db.models import signals +from django.conf import settings +from django.utils import simplejson as json +from django.core.serializers.json import DjangoJSONEncoder + +class JSONField(models.TextField): + """JSONField is a generic textfield that neatly serializes/unserializes + JSON objects seamlessly""" + + # Used so to_python() is called + __metaclass__ = models.SubfieldBase + + def __init__(self, json_type=None, **kwargs): + self.json_type = json_type + super(JSONField, self).__init__(**kwargs) + + def to_python(self, value): + """Convert our string value to JSON after we load it from the DB""" + + if value == "": + return None + + try: + if isinstance(value, basestring): + parsed_value = json.loads(value) + if self.json_type and parsed_value: + parsed_value = self.json_type.fromJSONDict(parsed_value) + + return parsed_value + except ValueError: + pass + + return value + + def get_db_prep_save(self, value): + """Convert our JSON object to a string before we save""" + + if value == "" or value == None: + return None + + if value and (self.json_type or hasattr(value, 'toJSONDict')): + value = value.toJSONDict() + + # if isinstance(value, dict): + value = json.dumps(value, cls=DjangoJSONEncoder) + + return super(JSONField, self).get_db_prep_save(value) diff --git a/auth/media/login-icons/facebook.png b/auth/media/login-icons/facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..156bfa141d47d4776a4394884a6b6b5c94774771 Binary files /dev/null and b/auth/media/login-icons/facebook.png differ diff --git a/auth/media/login-icons/google.png b/auth/media/login-icons/google.png new file mode 100644 index 0000000000000000000000000000000000000000..f6fbb5aeacd86436b8bf18c7fa0c9d5b9f160331 Binary files /dev/null and b/auth/media/login-icons/google.png differ diff --git a/auth/media/login-icons/password.png b/auth/media/login-icons/password.png new file mode 100644 index 0000000000000000000000000000000000000000..86f7807cbcf9f8d5add2c9450a1cf74b30ff1cb6 Binary files /dev/null and b/auth/media/login-icons/password.png differ diff --git a/auth/media/login-icons/twitter.png b/auth/media/login-icons/twitter.png new file mode 100644 index 0000000000000000000000000000000000000000..930edf2555535ae8a68896ddfbe6ece9aa4d00eb Binary files /dev/null and b/auth/media/login-icons/twitter.png differ diff --git a/auth/media/login-icons/yahoo.png b/auth/media/login-icons/yahoo.png new file mode 100644 index 0000000000000000000000000000000000000000..cfd329ced6d69e4acf16dd4be651f3b094946405 Binary files /dev/null and b/auth/media/login-icons/yahoo.png differ diff --git a/auth/media/main.css b/auth/media/main.css new file mode 100644 index 0000000000000000000000000000000000000000..54140e793681b951a169de216bafe604e8f9a665 --- /dev/null +++ b/auth/media/main.css @@ -0,0 +1,55 @@ + +body { + font-family: 'Lucida Grande',sans-serif; + background: white; + padding: 0px; + margin: 0px; +} + +#content { + position: absolute; + padding: 20px 30px 20px 30px; + top: 0px; + margin-left: 100px; + margin-top: 0px; + width: 860px; + background: #eee; + border-left: 1px solid #666; + border-right: 1px solid #666; + border-bottom: 1px solid #666; +} + +#header { + padding-top: 0px; + text-align: center; + padding-bottom: 20px; +} + +#footer { + border-top: 1px solid #666; + bottom: 0px; + margin: auto; + width: 860px; + text-align: center; + color: #666; + clear: both; +} + +#footer a, #footer a:visited { + color: black; + text-decoration: none; +} + +#footer a:hover { + text-decoration: underline; +} + +#page h2 { + background: #fc9; + border-bottom: 1px solid #666; + padding: 5px 0px 2px 5px; +} + +#election_info { + font-size: 16pt; +} \ No newline at end of file diff --git a/auth/media/signin-with-twitter.png b/auth/media/signin-with-twitter.png new file mode 100644 index 0000000000000000000000000000000000000000..746b6b9f80c71049f2a4eb84ff72d5d1bf77c62c Binary files /dev/null and b/auth/media/signin-with-twitter.png differ diff --git a/auth/media/twitter.png b/auth/media/twitter.png new file mode 100644 index 0000000000000000000000000000000000000000..62cf0036372324603b7fa238692b22d21f5dac26 Binary files /dev/null and b/auth/media/twitter.png differ diff --git a/auth/models.py b/auth/models.py new file mode 100644 index 0000000000000000000000000000000000000000..5b39a03f6df4c908409fd44b042add1ca2d66ad7 --- /dev/null +++ b/auth/models.py @@ -0,0 +1,147 @@ +""" +Data Objects for user authentication + +GAE + +Ben Adida +(ben@adida.net) +""" + +from django.db import models +from jsonfield import JSONField + +import datetime, logging + +from auth_systems import AUTH_SYSTEMS + + +class User(models.Model): + user_type = models.CharField(max_length=50) + user_id = models.CharField(max_length=100) + + name = models.CharField(max_length=200, null=True) + + # other properties + info = JSONField() + + # access token information + token = JSONField(null = True) + + # administrator + admin_p = models.BooleanField(default=False) + + class Meta: + unique_together = (('user_type', 'user_id'),) + + @classmethod + def _get_type_and_id(cls, user_type, user_id): + return "%s:%s" % (user_type, user_id) + + @property + def type_and_id(self): + return self._get_type_and_id(self.user_type, self.user_id) + + @classmethod + def get_by_type_and_id(cls, user_type, user_id): + return cls.objects.get(user_type = user_type, user_id = user_id) + + @classmethod + def update_or_create(cls, user_type, user_id, name=None, info=None, token=None): + obj, created_p = cls.objects.get_or_create(user_type = user_type, user_id = user_id, defaults = {'name': name, 'info':info, 'token':token}) + + if not created_p: + # special case the password: don't replace it if it exists + if obj.info.has_key('password'): + info['password'] = obj.info['password'] + + obj.info = info + obj.name = name + obj.token = token + obj.save() + + return obj + + def can_update_status(self): + if not AUTH_SYSTEMS.has_key(self.user_type): + return False + + return AUTH_SYSTEMS[self.user_type].STATUS_UPDATES + + def update_status_template(self): + if not self.can_update_status(): + return None + + return AUTH_SYSTEMS[self.user_type].STATUS_UPDATE_WORDING_TEMPLATE + + def update_status(self, status): + if AUTH_SYSTEMS.has_key(self.user_type): + AUTH_SYSTEMS[self.user_type].update_status(self.user_id, self.info, self.token, status) + + def send_message(self, subject, body): + if AUTH_SYSTEMS.has_key(self.user_type): + subject = subject.split("\n")[0] + AUTH_SYSTEMS[self.user_type].send_message(self.user_id, self.name, self.info, subject, body) + + def send_notification(self, message): + if AUTH_SYSTEMS.has_key(self.user_type): + if hasattr(AUTH_SYSTEMS[self.user_type], 'send_notification'): + AUTH_SYSTEMS[self.user_type].send_notification(self.user_id, self.info, message) + + def is_eligible_for(self, eligibility_case): + """ + Check if this user is eligible for this particular eligibility case, which looks like + {'auth_system': 'cas', 'constraint': [{}, {}, {}]} + and the constraints are OR'ed together + """ + + if eligibility_case['auth_system'] != self.user_type: + return False + + # no constraint? Then eligible! + if not eligibility_case.has_key('constraint'): + return True + + # from here on we know we match the auth system, but do we match one of the constraints? + + auth_system = AUTH_SYSTEMS[self.user_type] + + # does the auth system allow for checking a constraint? + if not hasattr(auth_system, 'check_constraint'): + return False + + for constraint in eligibility_case['constraint']: + # do we match on this constraint? + if auth_system.check_constraint(constraint=constraint, user_info = self.info): + return True + + # no luck + return False + + def __eq__(self, other): + if other: + return self.type_and_id == other.type_and_id + else: + return False + + + @property + def pretty_name(self): + if self.name: + return self.name + + if self.info.has_key('name'): + return self.info['name'] + + return self.user_id + + def _display_html(self, size): + return """<img border="0" height="%s" src="/static/auth/login-icons/%s.png" alt="%s" /> %s""" % ( + str(int(size)), self.user_type, self.user_type, self.pretty_name) + + @property + def display_html_small(self): + return self._display_html(15) + + @property + def display_html_big(self): + return self._display_html(25) diff --git a/auth/security/__init__.py b/auth/security/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1b3911cf4db0834dbb1cc991cf27607ac3a936d6 --- /dev/null +++ b/auth/security/__init__.py @@ -0,0 +1,123 @@ +""" +Generic Security -- for the auth system + +Ben Adida (ben@adida.net) +""" + +# nicely update the wrapper function +from functools import update_wrapper + +from django.http import HttpResponse, Http404, HttpResponseRedirect +from django.core.exceptions import * +from django.conf import settings + +import oauth + +import uuid + +from auth.models import * + +FIELDS_TO_SAVE = 'FIELDS_TO_SAVE' + +## FIXME: oauth is NOT working right now + +## +## OAuth and API clients +## + +class OAuthDataStore(oauth.OAuthDataStore): + def __init__(self): + pass + + def lookup_consumer(self, key): + c = APIClient.objects.get(consumer_key = key) + return oauth.OAuthConsumer(c.consumer_key, c.consumer_secret) + + def lookup_token(self, oauth_consumer, token_type, token): + if token_type != 'access': + raise NotImplementedError + + c = APIClient.objects.get(consumer_key = oauth_consumer.key) + return oauth.OAuthToken(c.consumer_key, c.consumer_secret) + + def lookup_nonce(self, oauth_consumer, oauth_token, nonce): + """ + FIXME this to actually check for nonces + """ + return None + +# create the oauth server +OAUTH_SERVER = oauth.OAuthServer(OAuthDataStore()) +OAUTH_SERVER.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1()) + +def get_api_client(request): + parameters = request.POST.copy() + parameters.update(request.GET) + + full_url = request.get_full_path() + + oauth_request = oauth.OAuthRequest.from_request(request.method, full_url, headers= request.META, + parameters=parameters, query_string=None) + + if not oauth_request: + return None + + try: + consumer, token, params = OAUTH_SERVER.verify_request(oauth_request) + return APIClient.objects.get(consumer_key = consumer.key) + except oauth.OAuthError: + return None + +# decorator for login required +def login_required(func): + def login_required_wrapper(request, *args, **kw): + if not (get_user(request) or get_api_client(request)): + return HttpResponseRedirect(settings.LOGIN_URL + "?return_url=" + request.get_full_path()) + + return func(request, *args, **kw) + + return update_wrapper(login_required_wrapper, func) + +# decorator for admin required +def admin_required(func): + def admin_required_wrapper(request, *args, **kw): + user = get_user(request) + if not user or not user.is_staff: + raise PermissionDenied() + + return func(request, *args, **kw) + + return update_wrapper(admin_required_wrapper, func) + +# get the user +def get_user(request): + # push the expiration of the session back + request.session.set_expiry(settings.SESSION_COOKIE_AGE) + + # set up CSRF protection if needed + if not request.session.has_key('csrf_token') or type(request.session['csrf_token']) != str: + request.session['csrf_token'] = str(uuid.uuid4()) + + if request.session.has_key('user'): + user = request.session['user'] + + # find the user + user_obj = User.get_by_type_and_id(user['type'], user['user_id']) + return user_obj + else: + return None + +def check_csrf(request): + if request.method != "POST": + return HttpResponseNotAllowed("only a POST for this URL") + + if (not request.POST.has_key('csrf_token')) or (request.POST['csrf_token'] != request.session['csrf_token']): + raise Exception("A CSRF problem was detected") + +def save_in_session_across_logouts(request, field_name, field_value): + fields_to_save = request.session.get(FIELDS_TO_SAVE, []) + if field_name not in fields_to_save: + fields_to_save.append(field_name) + request.session[FIELDS_TO_SAVE] = fields_to_save + + request.session[field_name] = field_value diff --git a/auth/security/oauth.py b/auth/security/oauth.py new file mode 100644 index 0000000000000000000000000000000000000000..04cc4bb2ba294c9f50a1eb91fe9ba9ef3e3aaa15 --- /dev/null +++ b/auth/security/oauth.py @@ -0,0 +1,539 @@ +""" +Initially downlaoded from +http://oauth.googlecode.com/svn/code/python/oauth/ + +Hacked a bit by Ben Adida (ben@adida.net) so that: +- access tokens are looked up with an extra param of consumer +""" + +import cgi +import urllib +import time +import random +import urlparse +import hmac +import base64 +import logging + +VERSION = '1.0' # Hi Blaine! +HTTP_METHOD = 'GET' +SIGNATURE_METHOD = 'PLAINTEXT' + +# Generic exception class +class OAuthError(RuntimeError): + def __init__(self, message='OAuth error occured.'): + self.message = message + +# optional WWW-Authenticate header (401 error) +def build_authenticate_header(realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + +# url escape +def escape(s): + # escape '/' too + return urllib.quote(s, safe='~') + +# util function: current timestamp +# seconds since epoch (UTC) +def generate_timestamp(): + return int(time.time()) + +# util function: nonce +# pseudorandom number +def generate_nonce(length=8): + return ''.join(str(random.randint(0, 9)) for i in range(length)) + +# OAuthConsumer is a data type that represents the identity of the Consumer +# via its shared secret with the Service Provider. +class OAuthConsumer(object): + key = None + secret = None + + def __init__(self, key, secret): + self.key = key + self.secret = secret + +# OAuthToken is a data type that represents an End User via either an access +# or request token. +class OAuthToken(object): + # access tokens and request tokens + key = None + secret = None + + ''' + key = the token + secret = the token secret + ''' + def __init__(self, key, secret): + self.key = key + self.secret = secret + + def to_string(self): + return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) + + # return a token from something like: + # oauth_token_secret=digg&oauth_token=digg + @staticmethod + def from_string(s): + params = cgi.parse_qs(s, keep_blank_values=False) + key = params['oauth_token'][0] + secret = params['oauth_token_secret'][0] + return OAuthToken(key, secret) + + def __str__(self): + return self.to_string() + +# OAuthRequest represents the request and can be serialized +class OAuthRequest(object): + ''' + OAuth parameters: + - oauth_consumer_key + - oauth_token + - oauth_signature_method + - oauth_signature + - oauth_timestamp + - oauth_nonce + - oauth_version + ... any additional parameters, as defined by the Service Provider. + ''' + parameters = None # oauth parameters + http_method = HTTP_METHOD + http_url = None + version = VERSION + + # added by Ben to filter out extra params from header + OAUTH_PARAMS = ['oauth_consumer_key', 'oauth_token', 'oauth_signature_method', 'oauth_signature', 'oauth_timestamp', 'oauth_nonce', 'oauth_version'] + + def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): + self.http_method = http_method + self.http_url = http_url + self.parameters = parameters or {} + + def set_parameter(self, parameter, value): + self.parameters[parameter] = value + + def get_parameter(self, parameter): + try: + return self.parameters[parameter] + except: + raise OAuthError('Parameter not found: %s' % parameter) + + def _get_timestamp_nonce(self): + return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') + + # get any non-oauth parameters + def get_nonoauth_parameters(self): + parameters = {} + for k, v in self.parameters.iteritems(): + # ignore oauth parameters + if k.find('oauth_') < 0: + parameters[k] = v + return parameters + + # serialize as a header for an HTTPAuth request + def to_header(self, realm=''): + auth_header = 'OAuth realm="%s"' % realm + # add the oauth parameters + if self.parameters: + for k, v in self.parameters.iteritems(): + # only if it's a standard OAUTH param (Ben) + if k in self.OAUTH_PARAMS: + auth_header += ', %s="%s"' % (k, escape(str(v))) + return {'Authorization': auth_header} + + # serialize as post data for a POST request + def to_postdata(self): + return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()) + + # serialize as a url for a GET request + def to_url(self): + return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) + + # return a string that consists of all the parameters that need to be signed + def get_normalized_parameters(self): + params = self.parameters + try: + # exclude the signature if it exists + del params['oauth_signature'] + except: + pass + key_values = params.items() + # sort lexicographically, first after key, then after value + key_values.sort() + # combine key value pairs in string and escape + return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values) + + # just uppercases the http method + def get_normalized_http_method(self): + return self.http_method.upper() + + # parses the url and rebuilds it to be scheme://host/path + def get_normalized_http_url(self): + parts = urlparse.urlparse(self.http_url) + url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path + return url_string + + # set the signature parameter to the result of build_signature + def sign_request(self, signature_method, consumer, token): + # set the signature method + self.set_parameter('oauth_signature_method', signature_method.get_name()) + # set the signature + self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) + + def build_signature(self, signature_method, consumer, token): + # call the build signature method within the signature method + return signature_method.build_signature(self, consumer, token) + + @staticmethod + def from_request(http_method, http_url, headers=None, parameters=None, query_string=None): + # combine multiple parameter sources + if parameters is None: + parameters = {} + + # headers + if headers and 'HTTP_AUTHORIZATION' in headers: + auth_header = headers['HTTP_AUTHORIZATION'] + # check that the authorization header is OAuth + if auth_header.index('OAuth') > -1: + try: + # get the parameters from the header + header_params = OAuthRequest._split_header(auth_header) + parameters.update(header_params) + except: + raise OAuthError('Unable to parse OAuth parameters from Authorization header.') + + # GET or POST query string + if query_string: + query_params = OAuthRequest._split_url_string(query_string) + parameters.update(query_params) + + # URL parameters + param_str = urlparse.urlparse(http_url)[4] # query + url_params = OAuthRequest._split_url_string(param_str) + parameters.update(url_params) + + if parameters: + return OAuthRequest(http_method, http_url, parameters) + + return None + + @staticmethod + def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + defaults = { + 'oauth_consumer_key': oauth_consumer.key, + 'oauth_timestamp': generate_timestamp(), + 'oauth_nonce': generate_nonce(), + 'oauth_version': OAuthRequest.version, + } + + defaults.update(parameters) + parameters = defaults + + if token: + parameters['oauth_token'] = token.key + + return OAuthRequest(http_method, http_url, parameters) + + @staticmethod + def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + parameters['oauth_token'] = token.key + + if callback: + parameters['oauth_callback'] = escape(callback) + + return OAuthRequest(http_method, http_url, parameters) + + # util function: turn Authorization: header into parameters, has to do some unescaping + @staticmethod + def _split_header(header): + params = {} + parts = header.split(',') + for param in parts: + # ignore realm parameter + if param.find('OAuth realm') > -1: + continue + # remove whitespace + param = param.strip() + # split key-value + param_parts = param.split('=', 1) + # remove quotes and unescape the value + params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) + return params + + # util function: turn url string into parameters, has to do some unescaping + @staticmethod + def _split_url_string(param_str): + parameters = cgi.parse_qs(param_str, keep_blank_values=False) + for k, v in parameters.iteritems(): + parameters[k] = urllib.unquote(v[0]) + return parameters + +# OAuthServer is a worker to check a requests validity against a data store +class OAuthServer(object): + timestamp_threshold = 300 # in seconds, five minutes + version = VERSION + signature_methods = None + data_store = None + + def __init__(self, data_store=None, signature_methods=None): + self.data_store = data_store + self.signature_methods = signature_methods or {} + + def set_data_store(self, oauth_data_store): + self.data_store = data_store + + def get_data_store(self): + return self.data_store + + def add_signature_method(self, signature_method): + self.signature_methods[signature_method.get_name()] = signature_method + return self.signature_methods + + # process a request_token request + # returns the request token on success + def fetch_request_token(self, oauth_request): + try: + # get the request token for authorization + token = self._get_token(oauth_request, 'request') + except OAuthError: + # no token required for the initial token request + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + self._check_signature(oauth_request, consumer, None) + # fetch a new token + token = self.data_store.fetch_request_token(consumer) + return token + + # process an access_token request + # returns the access token on success + def fetch_access_token(self, oauth_request): + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the request token + token = self._get_token(oauth_request, 'request') + self._check_signature(oauth_request, consumer, token) + new_token = self.data_store.fetch_access_token(consumer, token) + return new_token + + # verify an api call, checks all the parameters + def verify_request(self, oauth_request): + # -> consumer and token + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the access token + token = self._get_token(oauth_request, consumer, 'access') + self._check_signature(oauth_request, consumer, token) + parameters = oauth_request.get_nonoauth_parameters() + return consumer, token, parameters + + # authorize a request token + def authorize_token(self, token, user): + return self.data_store.authorize_request_token(token, user) + + # get the callback url + def get_callback(self, oauth_request): + return oauth_request.get_parameter('oauth_callback') + + # optional support for the authenticate header + def build_authenticate_header(self, realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + + # verify the correct version request for this server + def _get_version(self, oauth_request): + try: + version = oauth_request.get_parameter('oauth_version') + except: + version = VERSION + if version and version != self.version: + raise OAuthError('OAuth version %s not supported.' % str(version)) + return version + + # figure out the signature with some defaults + def _get_signature_method(self, oauth_request): + try: + signature_method = oauth_request.get_parameter('oauth_signature_method') + except: + signature_method = SIGNATURE_METHOD + try: + # get the signature method object + signature_method = self.signature_methods[signature_method] + except: + signature_method_names = ', '.join(self.signature_methods.keys()) + raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) + + return signature_method + + def _get_consumer(self, oauth_request): + consumer_key = oauth_request.get_parameter('oauth_consumer_key') + if not consumer_key: + raise OAuthError('Invalid consumer key.') + consumer = self.data_store.lookup_consumer(consumer_key) + if not consumer: + raise OAuthError('Invalid consumer.') + return consumer + + # try to find the token for the provided request token key + def _get_token(self, oauth_request, consumer, token_type='access'): + token_field = oauth_request.get_parameter('oauth_token') + token = self.data_store.lookup_token(consumer, token_type, token_field) + if not token: + raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) + return token + + def _check_signature(self, oauth_request, consumer, token): + timestamp, nonce = oauth_request._get_timestamp_nonce() + self._check_timestamp(timestamp) + self._check_nonce(consumer, token, nonce) + signature_method = self._get_signature_method(oauth_request) + try: + signature = oauth_request.get_parameter('oauth_signature') + except: + raise OAuthError('Missing signature.') + # validate the signature + valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature) + if not valid_sig: + key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) + raise OAuthError('Invalid signature. Expected signature base string: %s' % base) + built = signature_method.build_signature(oauth_request, consumer, token) + + def _check_timestamp(self, timestamp): + # verify that timestamp is recentish + timestamp = int(timestamp) + now = int(time.time()) + lapsed = now - timestamp + if lapsed > self.timestamp_threshold: + raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) + + def _check_nonce(self, consumer, token, nonce): + # verify that the nonce is uniqueish + nonce = self.data_store.lookup_nonce(consumer, token, nonce) + if nonce: + raise OAuthError('Nonce already used: %s' % str(nonce)) + +# OAuthClient is a worker to attempt to execute a request +class OAuthClient(object): + consumer = None + token = None + + def __init__(self, oauth_consumer, oauth_token): + self.consumer = oauth_consumer + self.token = oauth_token + + def get_consumer(self): + return self.consumer + + def get_token(self): + return self.token + + def fetch_request_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def access_resource(self, oauth_request): + # -> some protected resource + raise NotImplementedError + +# OAuthDataStore is a database abstraction used to lookup consumers and tokens +class OAuthDataStore(object): + + def lookup_consumer(self, key): + # -> OAuthConsumer + raise NotImplementedError + + def lookup_token(self, oauth_consumer, token_type, token_token): + # -> OAuthToken + raise NotImplementedError + + def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): + # -> OAuthToken + raise NotImplementedError + + def fetch_request_token(self, oauth_consumer): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_consumer, oauth_token): + # -> OAuthToken + raise NotImplementedError + + def authorize_request_token(self, oauth_token, user): + # -> OAuthToken + raise NotImplementedError + +# OAuthSignatureMethod is a strategy class that implements a signature method +class OAuthSignatureMethod(object): + def get_name(self): + # -> str + raise NotImplementedError + + def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): + # -> str key, str raw + raise NotImplementedError + + def build_signature(self, oauth_request, oauth_consumer, oauth_token): + # -> str + raise NotImplementedError + + def check_signature(self, oauth_request, consumer, token, signature): + built = self.build_signature(oauth_request, consumer, token) + logging.info("built %s" % built) + logging.info("signature %s" % signature) + return built == signature + +class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): + + def get_name(self): + return 'HMAC-SHA1' + + def build_signature_base_string(self, oauth_request, consumer, token): + sig = ( + escape(oauth_request.get_normalized_http_method()), + escape(oauth_request.get_normalized_http_url()), + escape(oauth_request.get_normalized_parameters()), + ) + + key = '%s&' % escape(consumer.secret) + if token: + key += escape(token.secret) + raw = '&'.join(sig) + return key, raw + + def build_signature(self, oauth_request, consumer, token): + # build the base signature string + key, raw = self.build_signature_base_string(oauth_request, consumer, token) + + # hmac object + try: + import hashlib # 2.5 + hashed = hmac.new(key, raw, hashlib.sha1) + except: + import sha # deprecated + hashed = hmac.new(key, raw, sha) + + # calculate the digest base 64 + return base64.b64encode(hashed.digest()) + +class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): + + def get_name(self): + return 'PLAINTEXT' + + def build_signature_base_string(self, oauth_request, consumer, token): + # concatenate the consumer key and secret + sig = escape(consumer.secret) + '&' + if token: + sig = sig + escape(token.secret) + return sig + + def build_signature(self, oauth_request, consumer, token): + return self.build_signature_base_string(oauth_request, consumer, token) diff --git a/auth/templates/base.html b/auth/templates/base.html new file mode 100644 index 0000000000000000000000000000000000000000..cd838d1cc5ef96312762a9c3cf5ad2a4d7e00bf1 --- /dev/null +++ b/auth/templates/base.html @@ -0,0 +1,40 @@ +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html xmlns="http://www.w3.org/1999/xhtml" + dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}" + xml:lang="{% firstof LANGUAGE_CODE 'en' %}" + lang="{% firstof LANGUAGE_CODE 'en' %}"> + <head> + <title>{% block title %}Authentication{% endblock %} - IACR</title> + {% block css %} + <!-- + <link rel="stylesheet" type="text/css" media="screen, projection" href="{{ MEDIA_URL }}combined-{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}.css" /> + --> + <link rel="stylesheet" type="text/css" media="screen" href="/static/main.css"> + + <!--[if IE]> + <link rel="stylesheet" type="text/css" media="screen, projection" href="{{ MEDIA_URL }}ie.css"> + <![endif]--> + {% endblock %} + + {% block js %} + {% endblock %} + + {% block extra-head %}{% endblock %} + </head> + + <body> + <div id="content"> + <div id="header"> + {% block header %} + {% endblock %} + </div> + {% block content %}{% endblock %} + <div id="footer"> + </div> + </div> + + </body> +</html> diff --git a/auth/templates/index.html b/auth/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..3af9a06542be6ab3ce4c432b8e35058c0b215307 --- /dev/null +++ b/auth/templates/index.html @@ -0,0 +1,17 @@ +{% extends TEMPLATE_BASE %} + +{% block content %} +<h1>Authentication</h1> + +{% if user %} +<p style="font-size: 1.4em;"> + You are currently logged in as<br /><b>{{user.user_id}}</b> via <b>{{user.user_type}}</b>. +</p> +<p> + <a href="{% url auth.views.logout %}">logout</a> +</p> + +{% else %} +{% include "login_box.html" %} +{% endif %} +{% endblock %} diff --git a/auth/templates/login_box.html b/auth/templates/login_box.html new file mode 100644 index 0000000000000000000000000000000000000000..4fb7a6eba40ef78e3b8ce7fa5778333b1bedd14d --- /dev/null +++ b/auth/templates/login_box.html @@ -0,0 +1,26 @@ +{% if default_auth_system %} +<p><a href="{% url auth.views.start system_name=default_auth_system %}?return_url={{return_url}}"> +{{default_auth_system_obj.LOGIN_MESSAGE}} +</a></p> +{% else %} +{% for auth_system in enabled_auth_systems %} +{% ifequal auth_system "password" %} +<form method="post" action="{% url auth.auth_systems.password.password_login_view %}"> +<input type="hidden" name="election_uuid" value="{{election.uuid}}" /> +<input type="hidden" name="csrf_token" value="{{csrf_token}}" /> +<input type="hidden" name="return_url" value="{{return_url}}" /> +<table> + {{form.as_table}} +</table> +<input type="submit" value="log in" /> +<a style="font-size: 0.8em;" href="{% url auth.auth_systems.password.password_forgotten_view %}?return_url={{return_url|urlencode}}">forgot password?</a> +</form> +{% else %} +<p> + <a href="{% url auth.views.start system_name=auth_system %}?return_url={{return_url}}" style="font-size: 1.4em;"> +<img border="0" height="35" src="/static/auth/login-icons/{{auth_system}}.png" alt="{{auth_system}}" /> {{auth_system}} +{% endifequal %} +</a> +</p> +{% endfor %} +{% endif %} diff --git a/auth/templates/password/forgot.html b/auth/templates/password/forgot.html new file mode 100644 index 0000000000000000000000000000000000000000..043a66789411c0aa0cd16923b8dfac3c19054ecb --- /dev/null +++ b/auth/templates/password/forgot.html @@ -0,0 +1,20 @@ +{% extends TEMPLATE_BASE %} + +{% block header %} +<h2>{{ settings.SITE_TITLE }} — Password Forgotten</h2> +{% endblock %} + +{% block content %} + +{% if error %} +<p style="border: 1px solid black; padding: 5px;"> + {{error}} +</p> +{% endif %} + +<form class="prettyform" action="" method="POST"> + <input type="hidden" name="csrf_token" value="{{csrf_token}}" /> + <input type="hidden" name="return_url" value="{{return_url}}" /> +Your Username : <input type="text" name="username" /> <input type="submit" value="Send Reminder" /> +</form> +{% endblock %} diff --git a/auth/templates/password/login.html b/auth/templates/password/login.html new file mode 100644 index 0000000000000000000000000000000000000000..9b038893d71d966c6a94a68eff78b0e9e4652a4e --- /dev/null +++ b/auth/templates/password/login.html @@ -0,0 +1,27 @@ +{% extends TEMPLATE_BASE %} + +{% block header %} +<h2>{{ settings.SITE_TITLE }} — Login</h2> +{% endblock %} + + +{% block content %} + +{% if error %} +<p style="border: 1px solid black; padding: 5px;"> + {{error}} +</p> +{% endif %} + +<p> +<form class="prettyform" action="" method="POST" id="create_election_form"> + <input type="hidden" name="csrf_token" value="{{csrf_token}}" /> +<table> + {{form.as_table}} +</table> +<div> +<label for=""> </label><input type="submit" value="Login" /> +</div> +</form> +</p> +{% endblock %} diff --git a/auth/templates/twitter/follow.html b/auth/templates/twitter/follow.html new file mode 100644 index 0000000000000000000000000000000000000000..bbda5da3db04fc18f9e2d26986e0e9dcafa2991e --- /dev/null +++ b/auth/templates/twitter/follow.html @@ -0,0 +1,25 @@ +{% extends TEMPLATE_BASE %} + +{% block header %} +<h2>{{ settings.SITE_TITLE }} — Twitter Follow?</h2> +{% endblock %} + + +{% block content %} + +<div style="font-size: 1.2em;"> +We recommend that you follow <b>@{{user_to_follow}}</b>.<br /><br /> +That way, {{reason_to_follow}}. We will not abuse of this friendship: only messages pertinent to you will be sent to you directly, and general updates happen only sporadically. +</div> +<br /><br /> +<form method="post" action=""> +<input type="hidden" name="csrf_token" value="{{csrf_token}}" /> +<span style="font-size: 1.3em;"> +<input type="checkbox" name="follow_p" value="1" checked> +follow @{{user_to_follow}} +<br /><br /> +<input type="submit" value="continue" /> +</span> +</form> + +{% endblock %} diff --git a/auth/tests.py b/auth/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..072e0701241a53ea6cf85d83c80bb316dbd082e9 --- /dev/null +++ b/auth/tests.py @@ -0,0 +1,74 @@ +""" +Unit Tests for Auth Systems +""" + +import unittest +import models + +from django.db import IntegrityError, transaction + +from auth_systems import AUTH_SYSTEMS + +class UserTests(unittest.TestCase): + + def setUp(self): + pass + + def test_unique_users(self): + """ + there should not be two users with the same user_type and user_id + """ + for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + models.User.objects.create(user_type = auth_system, user_id = 'foobar', info={'name':'Foo Bar'}) + + def double_insert(): + models.User.objects.create(user_type = auth_system, user_id = 'foobar', info={'name': 'Foo2 Bar'}) + + self.assertRaises(IntegrityError, double_insert) + transaction.rollback() + + def test_create_or_update(self): + """ + shouldn't create two users, and should reset the password + """ + for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_cou', info={'name':'Foo Bar'}) + + def double_update_or_create(): + new_name = 'Foo2 Bar' + u2 = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_cou', info={'name': new_name}) + + self.assertEquals(u.id, u2.id) + self.assertEquals(u2.info['name'], new_name) + + + def test_status_update(self): + """ + check that a user set up with status update ability reports it as such, + and otherwise does not report it + """ + for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_status_update', info={'name':'Foo Bar Status Update'}) + + if hasattr(auth_system_module, 'send_message'): + self.assertNotEquals(u.update_status_template, None) + else: + self.assertEquals(u.update_status_template, None) + + def test_eligibility(self): + """ + test that users are reported as eligible for something + + FIXME: also test constraints on eligibility + """ + for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_status_update', info={'name':'Foo Bar Status Update'}) + + self.assertTrue(u.is_eligible_for({'auth_system': auth_system})) + + def test_eq(self): + for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_eq', info={'name':'Foo Bar Status Update'}) + u2 = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_eq', info={'name':'Foo Bar Status Update'}) + + self.assertEquals(u, u2) diff --git a/auth/urls.py b/auth/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..cedf65e3d0d47548c898aae5ebc5cbcb23db83d5 --- /dev/null +++ b/auth/urls.py @@ -0,0 +1,29 @@ +""" +Authentication URLs + +Ben Adida (ben@adida.net) +""" + +from django.conf.urls.defaults import * + +from views import * +from auth_systems.password import password_login_view, password_forgotten_view +from auth_systems.twitter import follow_view + +urlpatterns = patterns('', + # basic static stuff + (r'^$', index), + (r'^logout$', logout), + (r'^start/(?P<system_name>.*)$', start), + (r'^after$', after), + (r'^after_intervention$', after_intervention), + + ## should make the following modular + + # password auth + (r'^password/login', password_login_view), + (r'^password/forgot', password_forgotten_view), + + # twitter + (r'^twitter/follow', follow_view), +) diff --git a/auth/utils.py b/auth/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..78a613a596f0b1deef8b3a3610269df038793e0f --- /dev/null +++ b/auth/utils.py @@ -0,0 +1,29 @@ +""" +Some basic utils +(previously were in helios module, but making things less interdependent + +2010-08-17 +""" + +from django.utils import simplejson + +## JSON +def to_json(d): + return simplejson.dumps(d, sort_keys=True) + +def from_json(json_str): + if not json_str: return None + return simplejson.loads(json_str) + +def JSONtoDict(json): + x=simplejson.loads(json) + return x + +def JSONFiletoDict(filename): + f = open(filename, 'r') + content = f.read() + f.close() + return JSONtoDict(content) + + + diff --git a/auth/view_utils.py b/auth/view_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e4bb20306a8a5f6840349449b5995e61c31cc4aa --- /dev/null +++ b/auth/view_utils.py @@ -0,0 +1,60 @@ +""" +Utilities for all views + +Ben Adida (12-30-2008) +""" + +from django.template import Context, Template, loader +from django.http import HttpResponse, Http404 +from django.shortcuts import render_to_response + +from auth.security import get_user + +import auth + +from django.conf import settings + +## +## BASICS +## + +SUCCESS = HttpResponse("SUCCESS") + +## +## template abstraction +## + +def prepare_vars(request, vars): + vars_with_user = vars.copy() + + if request: + vars_with_user['user'] = get_user(request) + vars_with_user['csrf_token'] = request.session['csrf_token'] + vars_with_user['SECURE_URL_HOST'] = settings.SECURE_URL_HOST + + vars_with_user['STATIC'] = '/static/auth' + vars_with_user['MEDIA_URL'] = '/static/auth/' + vars_with_user['TEMPLATE_BASE'] = auth.TEMPLATE_BASE + + vars_with_user['settings'] = settings + + return vars_with_user + +def render_template(request, template_name, vars = {}): + t = loader.get_template(template_name + '.html') + + vars_with_user = prepare_vars(request, vars) + + return render_to_response('auth/templates/%s.html' % template_name, vars_with_user) + +def render_template_raw(request, template_name, vars={}): + t = loader.get_template(template_name + '.html') + + vars_with_user = prepare_vars(request, vars) + c = Context(vars_with_user) + return t.render(c) + +def render_json(json_txt): + return HttpResponse(json_txt) + + diff --git a/auth/views.py b/auth/views.py new file mode 100644 index 0000000000000000000000000000000000000000..7aef37c3d9b55498e87e106e22d631655b6af2c5 --- /dev/null +++ b/auth/views.py @@ -0,0 +1,183 @@ +""" +Views for authentication + +Ben Adida +2009-07-05 +""" + +from django.http import * +from django.core.urlresolvers import reverse + +from view_utils import * +from auth.security import get_user + +import auth_systems +from auth_systems import AUTH_SYSTEMS +from auth_systems import password +import auth + +import copy + +from models import User + +from security import FIELDS_TO_SAVE + +def index(request): + """ + the page from which one chooses how to log in. + """ + + user = get_user(request) + + # single auth system? + if len(auth.ENABLED_AUTH_SYSTEMS) == 1 and not user: + return HttpResponseRedirect(reverse(start, args=[auth.ENABLED_AUTH_SYSTEMS[0]])+ '?return_url=' + request.GET.get('return_url', '')) + + #if auth.DEFAULT_AUTH_SYSTEM and not user: + # return HttpResponseRedirect(reverse(start, args=[auth.DEFAULT_AUTH_SYSTEM])+ '?return_url=' + request.GET.get('return_url', '')) + + default_auth_system_obj = None + if auth.DEFAULT_AUTH_SYSTEM: + default_auth_system_obj = auth_systems.AUTH_SYSTEMS[auth.DEFAULT_AUTH_SYSTEM] + + form = password.LoginForm() + + return render_template(request,'index', {'return_url' : request.GET.get('return_url', '/'), + 'enabled_auth_systems' : auth.ENABLED_AUTH_SYSTEMS, + 'default_auth_system': auth.DEFAULT_AUTH_SYSTEM, + 'default_auth_system_obj': default_auth_system_obj, + 'form' : form}) + +def login_box_raw(request, return_url='/', auth_systems = None): + """ + a chunk of HTML that shows the various login options + """ + default_auth_system_obj = None + if auth.DEFAULT_AUTH_SYSTEM: + default_auth_system_obj = auth_systems.AUTH_SYSTEMS[auth.DEFAULT_AUTH_SYSTEM] + + enabled_auth_systems = auth_systems or auth.ENABLED_AUTH_SYSTEMS + + form = password.LoginForm() + + return render_template_raw(request, 'login_box', { + 'enabled_auth_systems': enabled_auth_systems, 'return_url': return_url, + 'default_auth_system': auth.DEFAULT_AUTH_SYSTEM, 'default_auth_system_obj': default_auth_system_obj, + 'form' : form}) + +def do_local_logout(request): + """ + if there is a logged-in user, it is saved in the new session's "user_for_remote_logout" + variable. + """ + + user = None + + if request.session.has_key('user'): + user = request.session['user'] + + # 2010-08-14 be much more aggressive here + # we save a few fields across session renewals, + # but we definitely kill the session and renew + # the cookie + field_names_to_save = request.session.get(FIELDS_TO_SAVE, []) + fields_to_save = dict([(name, request.session.get(name, None)) for name in field_names_to_save]) + + # let's not forget to save the list of fields to save + field_names_to_save.append(FIELDS_TO_SAVE) + fields_to_save[FIELDS_TO_SAVE] = field_names_to_save + + request.session.flush() + + for name in field_names_to_save: + request.session[name] = fields_to_save[name] + + request.session['user_for_remote_logout'] = user + +def do_remote_logout(request, user, return_url="/"): + # FIXME: do something with return_url + auth_system = AUTH_SYSTEMS[user['type']] + + # does the auth system have a special logout procedure? + if hasattr(auth_system, 'do_logout'): + response = auth_system.do_logout(request.session.get('user_for_remote_logout', None)) + return response + +def do_complete_logout(request, return_url="/"): + do_local_logout(request) + user_for_remote_logout = request.session.get('user_for_remote_logout', None) + if user_for_remote_logout: + response = do_remote_logout(request, user_for_remote_logout, return_url) + return response + return None + +def logout(request): + """ + logout + """ + + return_url = request.GET.get('return_url',"/") + response = do_complete_logout(request, return_url) + if response: + return response + + return HttpResponseRedirect(return_url) + +def start(request, system_name): + if not (system_name in auth.ENABLED_AUTH_SYSTEMS): + return HttpResponseRedirect(reverse(index)) + + request.session.save() + + # store in the session the name of the system used for auth + request.session['auth_system_name'] = system_name + + # where to return to when done + request.session['auth_return_url'] = request.GET.get('return_url', '/') + + # get the system + system = AUTH_SYSTEMS[system_name] + + # where to send the user to? + redirect_url = "%s%s" % (settings.URL_HOST,reverse(after)) + auth_url = system.get_auth_url(request, redirect_url=redirect_url) + + if auth_url: + return HttpResponseRedirect(auth_url) + else: + return HttpResponse("an error occurred trying to contact " + system_name +", try again later") + +def after(request): + # which auth system were we using? + if not request.session.has_key('auth_system_name'): + do_local_logout(request) + return HttpResponseRedirect("/") + + system = AUTH_SYSTEMS[request.session['auth_system_name']] + + # get the user info + user = system.get_user_info_after_auth(request) + + if user: + # get the user and store any new data about him + user_obj = User.update_or_create(user['type'], user['user_id'], user['name'], user['info'], user['token']) + + request.session['user'] = user + else: + # we were logging out + pass + + # does the auth system want to present an additional view? + # this is, for example, to prompt the user to follow @heliosvoting + # so they can hear about election results + if hasattr(system, 'user_needs_intervention'): + intervention_response = system.user_needs_intervention(user['user_id'], user['info'], user['token']) + if intervention_response: + return intervention_response + + # go to the after intervention page. This is for modularity + return HttpResponseRedirect(reverse(after_intervention)) + +def after_intervention(request): + return HttpResponseRedirect(request.session['auth_return_url'] or "/") +