import json, urllib, urlparse from django import dispatch, http, shortcuts from django.conf import settings from django.contrib import auth, messages from django.contrib.auth import signals as auth_signals, views as auth_views from django.core import urlresolvers from django.template import loader from django.views import generic as generic_views from django.views.decorators import cache, csrf, debug from django.views.generic import edit as edit_views from django.utils import crypto, http as http_utils from django.utils.translation import ugettext_lazy as _ import bson import tweepy import django_browserid from django_browserid import views as browserid_views from . import backends, forms, models FACEBOOK_SCOPE = 'email' GOOGLE_SCOPE = 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile' class FacebookLoginView(generic_views.RedirectView): """ This view authenticates the user via Facebook. """ permanent = False def get_redirect_url(self, **kwargs): args = { 'client_id': settings.FACEBOOK_APP_ID, 'scope': FACEBOOK_SCOPE, 'redirect_uri': self.request.build_absolute_uri(urlresolvers.reverse('facebook_callback')), } return 'https://www.facebook.com/dialog/oauth?%s' % urllib.urlencode(args) class FacebookCallbackView(generic_views.RedirectView): """ Authentication callback. Redirects user to LOGIN_REDIRECT_URL. """ permanent = False # TODO: Redirect users to the page they initially came from url = settings.LOGIN_REDIRECT_URL def get(self, request, *args, **kwargs): # 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) if 'code' in request.GET: args = { 'client_id': settings.FACEBOOK_APP_ID, 'client_secret': settings.FACEBOOK_APP_SECRET, 'redirect_uri': request.build_absolute_uri(urlresolvers.reverse('facebook_callback')), 'code': request.GET['code'], } # Retrieve access token response = urlparse.parse_qs(urllib.urlopen('https://graph.facebook.com/oauth/access_token?%s' % urllib.urlencode(args)).read()) # TODO: Handle error, what if response does not contain access token? access_token = response['access_token'][0] user = auth.authenticate(facebook_access_token=access_token, request=request) assert user.is_authenticated() auth.login(request, user) return super(FacebookCallbackView, self).get(request, *args, **kwargs) else: # TODO: Message user that they have not been logged in because they cancelled the Facebook app # TODO: Use information provided by Facebook as to why the login was not successful return super(FacebookCallbackView, self).get(request, *args, **kwargs) class TwitterLoginView(generic_views.RedirectView): """ This view authenticates the user via Twitter. """ permanent = False def get_redirect_url(self, **kwargs): twitter_auth = tweepy.OAuthHandler( settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET, self.request.build_absolute_uri(urlresolvers.reverse('twitter_callback')), ) redirect_url = twitter_auth.get_authorization_url(signin_with_twitter=True) self.request.session['request_token'] = twitter_auth.request_token return redirect_url class TwitterCallbackView(generic_views.RedirectView): """ Authentication callback. Redirects user to TWITTER_LOGIN_REDIRECT. """ permanent = False # TODO: Redirect users to the page they initially came from url = settings.LOGIN_REDIRECT_URL def get(self, request, *args, **kwargs): if 'oauth_verifier' in request.GET: oauth_verifier = request.GET['oauth_verifier'] twitter_auth = tweepy.OAuthHandler(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET) request_token = request.session.pop('request_token') assert request_token.key == request.GET['oauth_token'] twitter_auth.set_request_token(request_token.key, request_token.secret) twitter_auth.get_access_token(verifier=oauth_verifier) user = auth.authenticate(twitter_access_token=twitter_auth.access_token, request=request) assert user.is_authenticated() auth.login(request, user) return super(TwitterCallbackView, self).get(request, *args, **kwargs) else: # TODO: Message user that they have not been logged in because they cancelled the twitter app # TODO: Use information provided from twitter as to why the login was not successful return super(TwitterCallbackView, self).get(request, *args, **kwargs) class GoogleLoginView(generic_views.RedirectView): """ This view authenticates the user via Google. """ permanent = False def get_redirect_url(self, **kwargs): args = { 'client_id': settings.GOOGLE_CLIENT_ID, 'scope': GOOGLE_SCOPE, 'redirect_uri': self.request.build_absolute_uri(urlresolvers.reverse('google_callback')), 'response_type': 'code', 'access_type': 'online', 'approval_prompt': 'auto', } return 'https://accounts.google.com/o/oauth2/auth?%s' % urllib.urlencode(args) class GoogleCallbackView(generic_views.RedirectView): """ Authentication callback. Redirects user to GOOGLE_REDIRECT_URL. """ permanent = False # TODO: Redirect users to the page they initially came from url = settings.LOGIN_REDIRECT_URL def get(self, request, *args, **kwargs): # 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) if 'code' in request.GET: args = { 'client_id': settings.GOOGLE_CLIENT_ID, 'client_secret': settings.GOOGLE_CLIENT_SECRET, 'redirect_uri': request.build_absolute_uri(urlresolvers.reverse('google_callback')), 'code': request.GET['code'], 'grant_type': 'authorization_code', } response = json.load(urllib.urlopen('https://accounts.google.com/o/oauth2/token', urllib.urlencode(args))) # TODO: Handle error, what if response does not contain access token? access_token = response['access_token'] user = auth.authenticate(google_access_token=access_token, request=request) assert user.is_authenticated() auth.login(request, user) return super(GoogleCallbackView, self).get(request, *args, **kwargs) else: # TODO: Message user that they have not been logged in because they cancelled the Google app # TODO: Use information provided from Google as to why the login was not successful return super(GoogleCallbackView, self).get(request, *args, **kwargs) class FoursquareLoginView(generic_views.RedirectView): """ This view authenticates the user via Foursquare. """ permanent = False def get_redirect_url(self, **kwargs): args = { 'client_id': settings.FOURSQUARE_CLIENT_ID, 'redirect_uri': self.request.build_absolute_uri(urlresolvers.reverse('foursquare_callback')), 'response_type': 'code', } return 'https://foursquare.com/oauth2/authenticate?%s' % urllib.urlencode(args) class FoursquareCallbackView(generic_views.RedirectView): """ Authentication callback. Redirects user to LOGIN_REDIRECT_URL. """ permanent = False # TODO: Redirect users to the page they initially came from url = settings.LOGIN_REDIRECT_URL def get(self, request, *args, **kwargs): if 'code' in request.GET: args = { 'client_id': settings.FOURSQUARE_CLIENT_ID, 'client_secret': settings.FOURSQUARE_CLIENT_SECRET, 'redirect_uri': request.build_absolute_uri(urlresolvers.reverse('foursquare_callback')), 'code': request.GET['code'], 'grant_type': 'authorization_code', } response = json.load(urllib.urlopen('https://foursquare.com/oauth2/access_token', urllib.urlencode(args))) # TODO: Handle error, what if response does not contain access token? access_token = response['access_token'] user = auth.authenticate(foursquare_access_token=access_token, request=request) assert user.is_authenticated() auth.login(request, user) return super(FoursquareCallbackView, self).get(request, *args, **kwargs) else: # TODO: Message user that they have not been logged in because they cancelled the foursquare app # TODO: Use information provided from foursquare as to why the login was not successful return super(FoursquareCallbackView, self).get(request, *args, **kwargs) class BrowserIDVerifyView(browserid_views.Verify): """ This view authenticates the user via Mozilla Persona (BrowserID). """ def form_valid(self, form): """Handles the return post request from the browserID form and puts interesting variables into the class. If everything checks out, then we call handle_user to decide how to handle a valid user """ self.assertion = form.cleaned_data['assertion'] self.audience = django_browserid.get_audience(self.request) self.user = auth.authenticate(browserid_assertion=self.assertion, browserid_audience=self.audience, request=self.request) assert self.user.is_authenticated() if self.user and self.user.is_active: return self.login_success() return self.login_failure() class RegistrationView(edit_views.FormView): """ This view checks if form data are valid, saves new user. New user is authenticated, logged in and redirected to home page. """ template_name = 'mongo_auth/registration.html' # TODO: Redirect users to the page they initially came from success_url = urlresolvers.reverse_lazy('home') form_class = forms.RegistrationForm def object_data(self, form): return { 'username': form.cleaned_data['username'], 'first_name': form.cleaned_data['first_name'], 'last_name': form.cleaned_data['last_name'], 'email': form.cleaned_data['email'], } def get_user_class(self): return backends.User def form_valid(self, form): new_user = self.get_user_class()(**self.object_data(form)) new_user.set_password(form.cleaned_data['password2']) new_user.save() # We update user with authentication data newuser = auth.authenticate(username=form.cleaned_data['username'], password=form.cleaned_data['password2']) assert newuser is not None, form.cleaned_data['username'] auth.login(self.request, newuser) messages.success(self.request, _("Registration has been successful.")) return super(RegistrationView, self).form_valid(form) def dispatch(self, request, *args, **kwargs): # TODO: Is this really the correct check? What is user is logged through third-party authentication, but still wants to register with us? if request.user.is_authenticated(): return http.HttpResponseRedirect(self.get_success_url()) return super(RegistrationView, self).dispatch(request, *args, **kwargs) class AccountChangeView(edit_views.FormView): """ This view displays form for updating user account. It checks if all fields are valid and updates it. """ template_name = 'mongo_auth/account.html' form_class = forms.AccountChangeForm success_url = urlresolvers.reverse_lazy('account') def form_valid(self, form): user = self.request.user user.first_name = form.cleaned_data['first_name'] user.last_name = form.cleaned_data['last_name'] if user.email != form.cleaned_data['email']: user.email_confirmed = False user.email = form.cleaned_data['email'] user.save() messages.success(self.request, _("Your account has been successfully updated.")) return super(AccountChangeView, self).form_valid(form) def dispatch(self, request, *args, **kwargs): # TODO: With lazy user support, we want users to be able to change their account even if not authenticated if not request.user.is_authenticated(): return shortcuts.redirect('login') return super(AccountChangeView, self).dispatch(request, *args, **kwargs) def get_form(self, form_class): return form_class(self.request.user, **self.get_form_kwargs()) def get_initial(self): return { 'first_name': self.request.user.first_name, 'last_name': self.request.user.last_name, 'email': self.request.user.email, } class PasswordChangeView(edit_views.FormView): """ This view displays form for changing password. """ template_name = 'mongo_auth/password_change.html' form_class = forms.PasswordChangeForm success_url = urlresolvers.reverse_lazy('account') def form_valid(self, form): self.request.user.set_password(form.cleaned_data['password1']) messages.success(self.request, _("Your password has been successfully changed.")) return super(PasswordChangeView, self).form_valid(form) def dispatch(self, request, *args, **kwargs): # 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? if not request.user.is_authenticated(): return shortcuts.redirect('login') return super(PasswordChangeView, self).dispatch(request, *args, **kwargs) def get_form(self, form_class): return form_class(self.request.user, **self.get_form_kwargs()) class EmailConfirmationSendToken(edit_views.FormView): template_name = 'mongo_auth/email_confirmation_send_token.html' form_class = forms.EmailConfirmationSendTokenForm success_url = urlresolvers.reverse_lazy('account') def form_valid(self, form): user = self.request.user confirmation_token = crypto.get_random_string(20) context = { 'CONFIRMATION_TOKEN_VALIDITY': models.CONFIRMATION_TOKEN_VALIDITY, 'EMAIL_SUBJECT_PREFIX': settings.EMAIL_SUBJECT_PREFIX, 'SITE_NAME': getattr(settings, 'SITE_NAME', None), 'confirmation_token': confirmation_token, 'email_address': user.email, 'request': self.request, 'user': user, } subject = loader.render_to_string('mongo_auth/confirmation_email_subject.txt', context) # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) email = loader.render_to_string('mongo_auth/confirmation_email.txt', context) user.email_confirmation_token = models.EmailConfirmationToken(value=confirmation_token) user.save() user.email_user(subject, email, allow_unconfirmed=True) messages.success(self.request, _("Confirmation e-mail has been sent to your e-mail address.")) return super(EmailConfirmationSendToken, self).form_valid(form) def dispatch(self, request, *args, **kwargs): # TODO: Allow e-mail address confirmation only if user has e-mail address defined return super(EmailConfirmationSendToken, self).dispatch(request, *args, **kwargs) class EmailConfirmationProcessToken(generic_views.FormView): template_name = 'mongo_auth/email_confirmation_process_token.html' form_class = forms.EmailConfirmationProcessTokenForm success_url = urlresolvers.reverse_lazy('account') def form_valid(self, form): user = self.request.user user.email_confirmed = True user.save() messages.success(self.request, _("You have successfully confirmed your e-mail address.")) return super(EmailConfirmationProcessToken, self).form_valid(form) def get_initial(self): return { 'confirmation_token': self.kwargs.get('confirmation_token'), } def dispatch(self, request, *args, **kwargs): # TODO: Allow e-mail address confirmation only if user has e-mail address defined # TODO: Check if currently logged in user is the same as the user requested the confirmation return super(EmailConfirmationProcessToken, self).dispatch(request, *args, **kwargs) def get_form(self, form_class): return form_class(self.request.user, **self.get_form_kwargs()) def logout(request): """ After user logouts, redirect her back to the page she came from. """ if request.method != 'POST': return http.HttpResponseBadRequest() url = request.POST.get(auth.REDIRECT_FIELD_NAME) return auth_views.logout_then_login(request, url) @csrf.csrf_protect def password_reset(request, post_reset_redirect=None, *args, **kwargs): if post_reset_redirect is None: post_reset_redirect = urlresolvers.reverse('password_reset') return auth_views.password_reset(request, post_reset_redirect=post_reset_redirect, *args, **kwargs) @debug.sensitive_post_parameters() @cache.never_cache def password_reset_confirm(request, *args, **kwargs): old_base36_to_int = http_utils.base36_to_int old_user = auth_views.User def base36_to_objectid(s): if 13 < len(s) <= 26: return bson.ObjectId(hex(int(s, 36))[2:-1]) else: return old_base36_to_int(s) http.base36_to_int = base36_to_objectid auth_views.base36_to_int = base36_to_objectid auth_views.User = backends.User try: result = auth_views.password_reset_confirm(request, *args, **kwargs) if isinstance(result, http.HttpResponseRedirect): messages.success(request, _("Your password has been set. You may go ahead and login now.")) return result finally: http.base36_to_int = old_base36_to_int auth_views.base36_to_int = old_base36_to_int auth_views.User = old_user @dispatch.receiver(auth_signals.user_logged_in) def user_login_message(sender, request, user, **kwargs): """ Shows success login message. """ # We fail silently because in tests messages middleware is not setup early enough messages.success(request, _("You have been successfully logged in."), fail_silently=True) @dispatch.receiver(auth_signals.user_logged_out) def user_logout_message(sender, request, user, **kwargs): """ Shows success logout message. """ # We fail silently because in tests messages middleware is not setup early enough messages.success(request, _("You have been successfully logged out."), fail_silently=True)