backends.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import json, urllib
  2. from django.conf import settings
  3. from django.core import exceptions
  4. from django.utils import crypto, importlib
  5. from mongoengine import queryset
  6. from mongoengine.django import auth
  7. import tweepy
  8. from django_browserid import auth as browserid_auth, base as browserid_base
  9. from . import models
  10. LAZYUSER_USERNAME_TEMPLATE = 'guest-%s'
  11. USER_CLASS = 'mongo_auth.models.User'
  12. def get_class(path):
  13. i = path.rfind('.')
  14. module, attr = path[:i], path[i+1:]
  15. try:
  16. mod = importlib.import_module(module)
  17. except ImportError, e:
  18. raise exceptions.ImproperlyConfigured('Error importing user class module %s: "%s"' % (path, e))
  19. except ValueError, e:
  20. raise exceptions.ImproperlyConfigured('Error importing user class module. Is USER_CLASS a correctly defined class path?')
  21. try:
  22. cls = getattr(mod, attr)
  23. except AttributeError:
  24. raise exceptions.ImproperlyConfigured('Module "%s" does not define user class "%s"' % (module, attr))
  25. return cls
  26. User = get_class(getattr(settings, 'USER_CLASS', USER_CLASS))
  27. class MongoEngineBackend(auth.MongoEngineBackend):
  28. # TODO: Implement object permission support
  29. supports_object_permissions = False
  30. # TODO: Implement anonymous user backend
  31. supports_anonymous_user = False
  32. # TODO: Implement inactive user backend
  33. supports_inactive_user = False
  34. def authenticate(self, username, password):
  35. user = self.user_class.objects(username__iexact=username).first()
  36. if user:
  37. if password and user.check_password(password):
  38. return user
  39. return None
  40. def get_user(self, user_id):
  41. try:
  42. return self.user_class.objects.with_id(user_id)
  43. except self.user_class.DoesNotExist:
  44. return None
  45. @property
  46. def user_class(self):
  47. return User
  48. class FacebookBackend(MongoEngineBackend):
  49. """
  50. Facebook authentication.
  51. Facebook uses strings 'male' and 'female' for representing user gender.
  52. """
  53. # TODO: List all profile data fields we (can) get
  54. def authenticate(self, facebook_access_token, request):
  55. # Retrieve user's profile information
  56. # TODO: Handle error, what if request was denied?
  57. facebook_profile_data = json.load(urllib.urlopen('https://graph.facebook.com/me?%s' % urllib.urlencode({'access_token': facebook_access_token})))
  58. try:
  59. user = self.user_class.objects.get(facebook_profile_data__id=facebook_profile_data.get('id'))
  60. except self.user_class.DoesNotExist:
  61. # TODO: Based on user preference, we might create a new user here, not just link with existing, if existing user is lazy user
  62. # We reload to make sure user object is recent
  63. request.user.reload()
  64. user = request.user
  65. # TODO: Is it OK to override Facebook link if it already exist with some other Facebook user?
  66. user.facebook_access_token = facebook_access_token
  67. user.facebook_profile_data = facebook_profile_data
  68. user.authenticate_facebook(request)
  69. user.save()
  70. return user
  71. class TwitterBackend(MongoEngineBackend):
  72. """
  73. Twitter authentication.
  74. Twitter profile data fields are:
  75. name: user's full name
  76. screen_name: user's username
  77. profile_image_url
  78. profile_background_image_url
  79. id: id of Twitter user
  80. id_str: string id of Twitter user
  81. created_at: full date of user's registration on Twitter
  82. location: user's location
  83. time_zone
  84. lang: user's preferred language
  85. url: URL of user's website
  86. description: user's description of themselves
  87. profile_sidebar_fill_color
  88. profile_text_color
  89. profile_background_color
  90. profile_link_color
  91. profile_sidebar_border_color
  92. friends_count
  93. followers_count
  94. statuses_count
  95. favourites_count
  96. listed_count
  97. notifications: boolean value
  98. geo_enabled: boolean value
  99. following: boolean value
  100. follow_request_sent: boolean value
  101. profile_use_background_image: boolean value
  102. verified: boolean value; tells whether user's email is verified
  103. protected: boolean value
  104. show_all_inline_media: boolean value
  105. is_translator: boolean value
  106. profile_background_tile: boolean value
  107. contributors_enabled: boolean value
  108. utc_offset: integer
  109. """
  110. def authenticate(self, twitter_access_token, request):
  111. TWITTER_CONSUMER_KEY = getattr(settings, 'TWITTER_CONSUMER_KEY', None)
  112. TWITTER_CONSUMER_SECRET = getattr(settings, 'TWITTER_CONSUMER_SECRET', None)
  113. if not TWITTER_CONSUMER_KEY or not TWITTER_CONSUMER_SECRET:
  114. return None
  115. twitter_auth = tweepy.OAuthHandler(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET)
  116. twitter_auth.set_access_token(twitter_access_token.key, twitter_access_token.secret)
  117. twitter_api = tweepy.API(twitter_auth)
  118. twitter_profile_data = twitter_api.me()
  119. if 'id' not in twitter_profile_data:
  120. return None
  121. try:
  122. user = self.user_class.objects.get(twitter_profile_data__id=twitter_profile_data['id'])
  123. except self.user_class.DoesNotExist:
  124. # TODO: Based on user preference, we might create a new user here, not just link with existing, if existing user is lazy user
  125. # We reload to make sure user object is recent
  126. request.user.reload()
  127. user = request.user
  128. # TODO: Is it OK to override Twitter link if it already exist with some other Twitter user?
  129. user.twitter_access_token = models.TwitterAccessToken(key=twitter_access_token.key, secret=twitter_access_token.secret)
  130. user.twitter_profile_data = twitter_profile_data
  131. user.authenticate_twitter(request)
  132. user.save()
  133. return user
  134. class GoogleBackend(MongoEngineBackend):
  135. """
  136. Google authentication.
  137. Google profile data fields are:
  138. family_name: last name
  139. given_name: first name
  140. name: full name
  141. link: URL of Google user profile page
  142. picture: URL of profile picture
  143. locale: the language Google user is using
  144. gender: the gender of Google user (other|female|male)
  145. timezone: the default timezone of Google user
  146. email: Google email of user
  147. id: id of Google user; should be a string
  148. verified_email: True, if email is verified by Google API
  149. """
  150. def authenticate(self, google_access_token, request):
  151. # Retrieve user's profile information
  152. # TODO: Handle error, what if request was denied?
  153. google_profile_data = json.load(urllib.urlopen('https://www.googleapis.com/oauth2/v1/userinfo?access_token=%s' % google_access_token))
  154. if 'id' not in google_profile_data:
  155. return None
  156. try:
  157. user = self.user_class.objects.get(google_profile_data__id=google_profile_data['id'])
  158. except self.user_class.DoesNotExist:
  159. # TODO: Based on user preference, we might create a new user here, not just link with existing, if existing user is lazy user
  160. # We reload to make sure user object is recent
  161. request.user.reload()
  162. user = request.user
  163. # TODO: Is it OK to override Google link if it already exist with some other Google user?
  164. user.google_access_token = google_access_token
  165. user.google_profile_data = google_profile_data
  166. user.authenticate_google(request)
  167. user.save()
  168. return user
  169. class FoursquareBackend(MongoEngineBackend):
  170. """
  171. Foursquare authentication.
  172. Foursquare profile data fields are:
  173. id: a unique identifier for this user.
  174. firstName: user's first name
  175. lastName: user's last name
  176. homeCity: user's home city
  177. photo: URL of a profile picture for this user
  178. gender: user's gender: male, female, or none
  179. relationship: the relationship of the acting user (me) to this user (them) (optional)
  180. type: one of page, celebrity, or user
  181. contact: an object containing none, some, or all of twitter, facebook, email, and phone
  182. pings: pings from this user (optional)
  183. badges: contains the count of badges for this user, may eventually contain some selected badges
  184. checkins: contains the count of checkins by this user, may contain the most recent checkin as an array
  185. mayorships: contains the count of mayorships for this user and an items array that for now is empty
  186. tips: contains the count of tips from this user, may contain an array of selected tips as items
  187. todos: contains the count of todos this user has, may contain an array of selected todos as items
  188. photos: contains the count of photos this user has, may contain an array of selected photos as items
  189. friends: contains count of friends for this user and groups of users who are friends
  190. followers: contains count of followers for this user, if they are a page or celebrity
  191. requests: contains count of pending friend requests for this user
  192. pageInfo: contains a detailed page, if they are a page
  193. """
  194. def authenticate(self, foursquare_access_token, request):
  195. # Retrieve user's profile information
  196. # TODO: Handle error, what if request was denied?
  197. foursquare_profile_data = json.load(urllib.urlopen('https://api.foursquare.com/v2/users/self?%s' % urllib.urlencode({'oauth_token': foursquare_access_token})))['response']['user']
  198. if 'id' not in foursquare_profile_data:
  199. return None
  200. try:
  201. user = self.user_class.objects.get(foursquare_profile_data__id=foursquare_profile_data['id'])
  202. except self.user_class.DoesNotExist:
  203. # TODO: Based on user preference, we might create a new user here, not just link with existing, if existing user is lazy user
  204. # We reload to make sure user object is recent
  205. request.user.reload()
  206. user = request.user
  207. # TODO: Is it OK to override Foursquare link if it already exist with some other Foursquare user?
  208. user.foursquare_access_token = foursquare_access_token
  209. user.foursquare_profile_data = foursquare_profile_data
  210. user.authenticate_foursquare(request)
  211. user.save()
  212. return user
  213. class BrowserIDBackend(MongoEngineBackend, browserid_auth.BrowserIDBackend):
  214. """
  215. Persona authentication.
  216. Persona profile data fields are:
  217. email: email user uses for Persona
  218. """
  219. def authenticate(self, browserid_assertion=None, browserid_audience=None, request=None):
  220. browserid_profile_data = browserid_base.verify(browserid_assertion, browserid_audience)
  221. if not browserid_profile_data or 'email' not in browserid_profile_data:
  222. return None
  223. try:
  224. user = self.user_class.objects.get(browserid_profile_data__email=browserid_profile_data['email'])
  225. # TODO: What is we get more than one user?
  226. except self.user_class.DoesNotExist:
  227. # TODO: Based on user preference, we might create a new user here, not just link with existing, if existing user is lazy user
  228. # We reload to make sure user object is recent
  229. request.user.reload()
  230. user = request.user
  231. # TODO: Is it OK to override BrowserID link if it already exist with some other BrowserID user?
  232. user.browserid_profile_data = browserid_profile_data
  233. user.authenticate_browserid(request)
  234. user.save()
  235. return user
  236. class LazyUserBackend(MongoEngineBackend):
  237. def authenticate(self, request):
  238. while True:
  239. try:
  240. username = LAZYUSER_USERNAME_TEMPLATE % crypto.get_random_string(6)
  241. user = self.user_class.create_user(username=username, lazyuser=True)
  242. user.authenticate_lazyuser(request)
  243. user.save()
  244. break
  245. except queryset.OperationError, e:
  246. msg = str(e)
  247. if 'E11000' in msg and 'duplicate key error' in msg and 'username' in msg:
  248. continue
  249. raise
  250. return user