views.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. import json, urllib, urlparse
  2. from django import dispatch, http, shortcuts
  3. from django.conf import settings
  4. from django.contrib import auth, messages
  5. from django.contrib.auth import signals as auth_signals, views as auth_views
  6. from django.core import urlresolvers
  7. from django.template import loader
  8. from django.views import generic as generic_views
  9. from django.views.decorators import cache, csrf, debug
  10. from django.views.generic import edit as edit_views
  11. from django.utils import crypto, http as http_utils
  12. from django.utils.translation import ugettext_lazy as _
  13. import bson
  14. import tweepy
  15. import django_browserid
  16. from django_browserid import views as browserid_views
  17. from . import backends, forms, models
  18. FACEBOOK_SCOPE = 'email'
  19. GOOGLE_SCOPE = 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile'
  20. class FacebookLoginView(generic_views.RedirectView):
  21. """
  22. This view authenticates the user via Facebook.
  23. """
  24. permanent = False
  25. def get_redirect_url(self, **kwargs):
  26. args = {
  27. 'client_id': settings.FACEBOOK_APP_ID,
  28. 'scope': FACEBOOK_SCOPE,
  29. 'redirect_uri': self.request.build_absolute_uri(urlresolvers.reverse('facebook_callback')),
  30. }
  31. return 'https://www.facebook.com/dialog/oauth?%s' % urllib.urlencode(args)
  32. class FacebookCallbackView(generic_views.RedirectView):
  33. """
  34. Authentication callback. Redirects user to LOGIN_REDIRECT_URL.
  35. """
  36. permanent = False
  37. # TODO: Redirect users to the page they initially came from
  38. url = settings.LOGIN_REDIRECT_URL
  39. def get(self, request, *args, **kwargs):
  40. # TODO: Add security measures to prevent attackers from sending a redirect to this url with a forged 'code' (you can use 'state' parameter to set a random nonce and store it into session)
  41. if 'code' in request.GET:
  42. args = {
  43. 'client_id': settings.FACEBOOK_APP_ID,
  44. 'client_secret': settings.FACEBOOK_APP_SECRET,
  45. 'redirect_uri': request.build_absolute_uri(urlresolvers.reverse('facebook_callback')),
  46. 'code': request.GET['code'],
  47. }
  48. # Retrieve access token
  49. response = urlparse.parse_qs(urllib.urlopen('https://graph.facebook.com/oauth/access_token?%s' % urllib.urlencode(args)).read())
  50. # TODO: Handle error, what if response does not contain access token?
  51. access_token = response['access_token'][0]
  52. user = auth.authenticate(facebook_access_token=access_token, request=request)
  53. assert user.is_authenticated()
  54. auth.login(request, user)
  55. return super(FacebookCallbackView, self).get(request, *args, **kwargs)
  56. else:
  57. # TODO: Message user that they have not been logged in because they cancelled the Facebook app
  58. # TODO: Use information provided by Facebook as to why the login was not successful
  59. return super(FacebookCallbackView, self).get(request, *args, **kwargs)
  60. class TwitterLoginView(generic_views.RedirectView):
  61. """
  62. This view authenticates the user via Twitter.
  63. """
  64. permanent = False
  65. def get_redirect_url(self, **kwargs):
  66. twitter_auth = tweepy.OAuthHandler(
  67. settings.TWITTER_CONSUMER_KEY,
  68. settings.TWITTER_CONSUMER_SECRET,
  69. self.request.build_absolute_uri(urlresolvers.reverse('twitter_callback')),
  70. )
  71. redirect_url = twitter_auth.get_authorization_url(signin_with_twitter=True)
  72. self.request.session['request_token'] = twitter_auth.request_token
  73. return redirect_url
  74. class TwitterCallbackView(generic_views.RedirectView):
  75. """
  76. Authentication callback. Redirects user to TWITTER_LOGIN_REDIRECT.
  77. """
  78. permanent = False
  79. # TODO: Redirect users to the page they initially came from
  80. url = settings.LOGIN_REDIRECT_URL
  81. def get(self, request, *args, **kwargs):
  82. if 'oauth_verifier' in request.GET:
  83. oauth_verifier = request.GET['oauth_verifier']
  84. twitter_auth = tweepy.OAuthHandler(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET)
  85. request_token = request.session.pop('request_token')
  86. assert request_token.key == request.GET['oauth_token']
  87. twitter_auth.set_request_token(request_token.key, request_token.secret)
  88. twitter_auth.get_access_token(verifier=oauth_verifier)
  89. user = auth.authenticate(twitter_access_token=twitter_auth.access_token, request=request)
  90. assert user.is_authenticated()
  91. auth.login(request, user)
  92. return super(TwitterCallbackView, self).get(request, *args, **kwargs)
  93. else:
  94. # TODO: Message user that they have not been logged in because they cancelled the twitter app
  95. # TODO: Use information provided from twitter as to why the login was not successful
  96. return super(TwitterCallbackView, self).get(request, *args, **kwargs)
  97. class GoogleLoginView(generic_views.RedirectView):
  98. """
  99. This view authenticates the user via Google.
  100. """
  101. permanent = False
  102. def get_redirect_url(self, **kwargs):
  103. args = {
  104. 'client_id': settings.GOOGLE_CLIENT_ID,
  105. 'scope': GOOGLE_SCOPE,
  106. 'redirect_uri': self.request.build_absolute_uri(urlresolvers.reverse('google_callback')),
  107. 'response_type': 'code',
  108. 'access_type': 'online',
  109. 'approval_prompt': 'auto',
  110. }
  111. return 'https://accounts.google.com/o/oauth2/auth?%s' % urllib.urlencode(args)
  112. class GoogleCallbackView(generic_views.RedirectView):
  113. """
  114. Authentication callback. Redirects user to GOOGLE_REDIRECT_URL.
  115. """
  116. permanent = False
  117. # TODO: Redirect users to the page they initially came from
  118. url = settings.LOGIN_REDIRECT_URL
  119. def get(self, request, *args, **kwargs):
  120. # TODO: Add security measures to prevent attackers from sending a redirect to this url with a forged 'code' (you can use 'state' parameter to set a random nonce and store it into session)
  121. if 'code' in request.GET:
  122. args = {
  123. 'client_id': settings.GOOGLE_CLIENT_ID,
  124. 'client_secret': settings.GOOGLE_CLIENT_SECRET,
  125. 'redirect_uri': request.build_absolute_uri(urlresolvers.reverse('google_callback')),
  126. 'code': request.GET['code'],
  127. 'grant_type': 'authorization_code',
  128. }
  129. response = json.load(urllib.urlopen('https://accounts.google.com/o/oauth2/token', urllib.urlencode(args)))
  130. # TODO: Handle error, what if response does not contain access token?
  131. access_token = response['access_token']
  132. user = auth.authenticate(google_access_token=access_token, request=request)
  133. assert user.is_authenticated()
  134. auth.login(request, user)
  135. return super(GoogleCallbackView, self).get(request, *args, **kwargs)
  136. else:
  137. # TODO: Message user that they have not been logged in because they cancelled the Google app
  138. # TODO: Use information provided from Google as to why the login was not successful
  139. return super(GoogleCallbackView, self).get(request, *args, **kwargs)
  140. class FoursquareLoginView(generic_views.RedirectView):
  141. """
  142. This view authenticates the user via Foursquare.
  143. """
  144. permanent = False
  145. def get_redirect_url(self, **kwargs):
  146. args = {
  147. 'client_id': settings.FOURSQUARE_CLIENT_ID,
  148. 'redirect_uri': self.request.build_absolute_uri(urlresolvers.reverse('foursquare_callback')),
  149. 'response_type': 'code',
  150. }
  151. return 'https://foursquare.com/oauth2/authenticate?%s' % urllib.urlencode(args)
  152. class FoursquareCallbackView(generic_views.RedirectView):
  153. """
  154. Authentication callback. Redirects user to LOGIN_REDIRECT_URL.
  155. """
  156. permanent = False
  157. # TODO: Redirect users to the page they initially came from
  158. url = settings.LOGIN_REDIRECT_URL
  159. def get(self, request, *args, **kwargs):
  160. if 'code' in request.GET:
  161. args = {
  162. 'client_id': settings.FOURSQUARE_CLIENT_ID,
  163. 'client_secret': settings.FOURSQUARE_CLIENT_SECRET,
  164. 'redirect_uri': request.build_absolute_uri(urlresolvers.reverse('foursquare_callback')),
  165. 'code': request.GET['code'],
  166. 'grant_type': 'authorization_code',
  167. }
  168. response = json.load(urllib.urlopen('https://foursquare.com/oauth2/access_token', urllib.urlencode(args)))
  169. # TODO: Handle error, what if response does not contain access token?
  170. access_token = response['access_token']
  171. user = auth.authenticate(foursquare_access_token=access_token, request=request)
  172. assert user.is_authenticated()
  173. auth.login(request, user)
  174. return super(FoursquareCallbackView, self).get(request, *args, **kwargs)
  175. else:
  176. # TODO: Message user that they have not been logged in because they cancelled the foursquare app
  177. # TODO: Use information provided from foursquare as to why the login was not successful
  178. return super(FoursquareCallbackView, self).get(request, *args, **kwargs)
  179. class BrowserIDVerifyView(browserid_views.Verify):
  180. """
  181. This view authenticates the user via Mozilla Persona (BrowserID).
  182. """
  183. def form_valid(self, form):
  184. """Handles the return post request from the browserID form and puts
  185. interesting variables into the class. If everything checks out, then
  186. we call handle_user to decide how to handle a valid user
  187. """
  188. self.assertion = form.cleaned_data['assertion']
  189. self.audience = django_browserid.get_audience(self.request)
  190. self.user = auth.authenticate(browserid_assertion=self.assertion, browserid_audience=self.audience, request=self.request)
  191. assert self.user.is_authenticated()
  192. if self.user and self.user.is_active:
  193. return self.login_success()
  194. return self.login_failure()
  195. class RegistrationView(edit_views.FormView):
  196. """
  197. This view checks if form data are valid, saves new user.
  198. New user is authenticated, logged in and redirected to home page.
  199. """
  200. template_name = 'mongo_auth/registration.html'
  201. # TODO: Redirect users to the page they initially came from
  202. success_url = urlresolvers.reverse_lazy('home')
  203. form_class = forms.RegistrationForm
  204. def object_data(self, form):
  205. return {
  206. 'username': form.cleaned_data['username'],
  207. 'first_name': form.cleaned_data['first_name'],
  208. 'last_name': form.cleaned_data['last_name'],
  209. 'email': form.cleaned_data['email'],
  210. }
  211. def get_user_class(self):
  212. return backends.User
  213. def form_valid(self, form):
  214. new_user = self.get_user_class()(**self.object_data(form))
  215. new_user.set_password(form.cleaned_data['password2'])
  216. new_user.save()
  217. # We update user with authentication data
  218. newuser = auth.authenticate(username=form.cleaned_data['username'], password=form.cleaned_data['password2'])
  219. assert newuser is not None, form.cleaned_data['username']
  220. auth.login(self.request, newuser)
  221. messages.success(self.request, _("Registration has been successful."))
  222. return super(RegistrationView, self).form_valid(form)
  223. def dispatch(self, request, *args, **kwargs):
  224. # TODO: Is this really the correct check? What is user is logged through third-party authentication, but still wants to register with us?
  225. if request.user.is_authenticated():
  226. return http.HttpResponseRedirect(self.get_success_url())
  227. return super(RegistrationView, self).dispatch(request, *args, **kwargs)
  228. class AccountChangeView(edit_views.FormView):
  229. """
  230. This view displays form for updating user account. It checks if all fields are valid and updates it.
  231. """
  232. template_name = 'mongo_auth/account.html'
  233. form_class = forms.AccountChangeForm
  234. success_url = urlresolvers.reverse_lazy('account')
  235. def form_valid(self, form):
  236. user = self.request.user
  237. user.first_name = form.cleaned_data['first_name']
  238. user.last_name = form.cleaned_data['last_name']
  239. if user.email != form.cleaned_data['email']:
  240. user.email_confirmed = False
  241. user.email = form.cleaned_data['email']
  242. user.save()
  243. messages.success(self.request, _("Your account has been successfully updated."))
  244. return super(AccountChangeView, self).form_valid(form)
  245. def dispatch(self, request, *args, **kwargs):
  246. # TODO: With lazy user support, we want users to be able to change their account even if not authenticated
  247. if not request.user.is_authenticated():
  248. return shortcuts.redirect('login')
  249. return super(AccountChangeView, self).dispatch(request, *args, **kwargs)
  250. def get_form(self, form_class):
  251. return form_class(self.request.user, **self.get_form_kwargs())
  252. def get_initial(self):
  253. return {
  254. 'first_name': self.request.user.first_name,
  255. 'last_name': self.request.user.last_name,
  256. 'email': self.request.user.email,
  257. }
  258. class PasswordChangeView(edit_views.FormView):
  259. """
  260. This view displays form for changing password.
  261. """
  262. template_name = 'mongo_auth/password_change.html'
  263. form_class = forms.PasswordChangeForm
  264. success_url = urlresolvers.reverse_lazy('account')
  265. def form_valid(self, form):
  266. self.request.user.set_password(form.cleaned_data['password1'])
  267. messages.success(self.request, _("Your password has been successfully changed."))
  268. return super(PasswordChangeView, self).form_valid(form)
  269. def dispatch(self, request, *args, **kwargs):
  270. # TODO: Is this really the correct check? What is user is logged through third-party authentication, but still does not have current password - is not then changing password the same as registration?
  271. if not request.user.is_authenticated():
  272. return shortcuts.redirect('login')
  273. return super(PasswordChangeView, self).dispatch(request, *args, **kwargs)
  274. def get_form(self, form_class):
  275. return form_class(self.request.user, **self.get_form_kwargs())
  276. class EmailConfirmationSendToken(edit_views.FormView):
  277. template_name = 'mongo_auth/email_confirmation_send_token.html'
  278. form_class = forms.EmailConfirmationSendTokenForm
  279. success_url = urlresolvers.reverse_lazy('account')
  280. def form_valid(self, form):
  281. user = self.request.user
  282. confirmation_token = crypto.get_random_string(20)
  283. context = {
  284. 'CONFIRMATION_TOKEN_VALIDITY': models.CONFIRMATION_TOKEN_VALIDITY,
  285. 'EMAIL_SUBJECT_PREFIX': settings.EMAIL_SUBJECT_PREFIX,
  286. 'SITE_NAME': getattr(settings, 'SITE_NAME', None),
  287. 'confirmation_token': confirmation_token,
  288. 'email_address': user.email,
  289. 'request': self.request,
  290. 'user': user,
  291. }
  292. subject = loader.render_to_string('mongo_auth/confirmation_email_subject.txt', context)
  293. # Email subject *must not* contain newlines
  294. subject = ''.join(subject.splitlines())
  295. email = loader.render_to_string('mongo_auth/confirmation_email.txt', context)
  296. user.email_confirmation_token = models.EmailConfirmationToken(value=confirmation_token)
  297. user.save()
  298. user.email_user(subject, email, allow_unconfirmed=True)
  299. messages.success(self.request, _("Confirmation e-mail has been sent to your e-mail address."))
  300. return super(EmailConfirmationSendToken, self).form_valid(form)
  301. def dispatch(self, request, *args, **kwargs):
  302. # TODO: Allow e-mail address confirmation only if user has e-mail address defined
  303. return super(EmailConfirmationSendToken, self).dispatch(request, *args, **kwargs)
  304. class EmailConfirmationProcessToken(generic_views.FormView):
  305. template_name = 'mongo_auth/email_confirmation_process_token.html'
  306. form_class = forms.EmailConfirmationProcessTokenForm
  307. success_url = urlresolvers.reverse_lazy('account')
  308. def form_valid(self, form):
  309. user = self.request.user
  310. user.email_confirmed = True
  311. user.save()
  312. messages.success(self.request, _("You have successfully confirmed your e-mail address."))
  313. return super(EmailConfirmationProcessToken, self).form_valid(form)
  314. def get_initial(self):
  315. return {
  316. 'confirmation_token': self.kwargs.get('confirmation_token'),
  317. }
  318. def dispatch(self, request, *args, **kwargs):
  319. # TODO: Allow e-mail address confirmation only if user has e-mail address defined
  320. # TODO: Check if currently logged in user is the same as the user requested the confirmation
  321. return super(EmailConfirmationProcessToken, self).dispatch(request, *args, **kwargs)
  322. def get_form(self, form_class):
  323. return form_class(self.request.user, **self.get_form_kwargs())
  324. def logout(request):
  325. """
  326. After user logouts, redirect her back to the page she came from.
  327. """
  328. if request.method != 'POST':
  329. return http.HttpResponseBadRequest()
  330. url = request.POST.get(auth.REDIRECT_FIELD_NAME)
  331. return auth_views.logout_then_login(request, url)
  332. @csrf.csrf_protect
  333. def password_reset(request, post_reset_redirect=None, *args, **kwargs):
  334. if post_reset_redirect is None:
  335. post_reset_redirect = urlresolvers.reverse('password_reset')
  336. return auth_views.password_reset(request, post_reset_redirect=post_reset_redirect, *args, **kwargs)
  337. @debug.sensitive_post_parameters()
  338. @cache.never_cache
  339. def password_reset_confirm(request, *args, **kwargs):
  340. old_base36_to_int = http_utils.base36_to_int
  341. old_user = auth_views.User
  342. def base36_to_objectid(s):
  343. if 13 < len(s) <= 26:
  344. return bson.ObjectId(hex(int(s, 36))[2:-1])
  345. else:
  346. return old_base36_to_int(s)
  347. http.base36_to_int = base36_to_objectid
  348. auth_views.base36_to_int = base36_to_objectid
  349. auth_views.User = backends.User
  350. try:
  351. result = auth_views.password_reset_confirm(request, *args, **kwargs)
  352. if isinstance(result, http.HttpResponseRedirect):
  353. messages.success(request, _("Your password has been set. You may go ahead and login now."))
  354. return result
  355. finally:
  356. http.base36_to_int = old_base36_to_int
  357. auth_views.base36_to_int = old_base36_to_int
  358. auth_views.User = old_user
  359. @dispatch.receiver(auth_signals.user_logged_in)
  360. def user_login_message(sender, request, user, **kwargs):
  361. """
  362. Shows success login message.
  363. """
  364. # We fail silently because in tests messages middleware is not setup early enough
  365. messages.success(request, _("You have been successfully logged in."), fail_silently=True)
  366. @dispatch.receiver(auth_signals.user_logged_out)
  367. def user_logout_message(sender, request, user, **kwargs):
  368. """
  369. Shows success logout message.
  370. """
  371. # We fail silently because in tests messages middleware is not setup early enough
  372. messages.success(request, _("You have been successfully logged out."), fail_silently=True)