Files
PyRIGS/DiscourseAuth/views.py
David Taylor 6b7889d80e Add copy of Discourse-Auth app,
Probably should split into separate repo at some point, but committing here so I don't lose it
2017-04-11 00:28:35 +01:00

254 lines
9.1 KiB
Python

from django.views.generic import View, FormView, TemplateView
from django.http import HttpResponseRedirect
from django.contrib.auth import login
from django.contrib.auth.backends import ModelBackend
from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied
from django.contrib.auth import get_user_model
import urllib
from hashlib import sha256
import hmac
from base64 import b64decode, b64encode
from django.db.models import Q
from django.conf import settings
from django.core.exceptions import ValidationError
from django.contrib import messages
from models import AuthAttempt, DiscourseUserLink
import time
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from registration.forms import RegistrationForm
class StartDiscourseAuth(View):
http_method_names = ['get']
def get(self, request, *args, **kwargs):
# Where do we want to go once authentication is complete?
request.session['discourse_next'] = request.GET.get('next', "/")
# Generate random 'nonce'
attempt = AuthAttempt.objects.create()
nonce = attempt.nonce
# Where do we want discourse to send authenticated users?
redirect_uri = reverse('continue-auth')
redirect_uri = request.build_absolute_uri(redirect_uri)
# Data to sent to Discourse (payload)
data = {
'nonce': nonce,
'return_sso_url': redirect_uri
}
payload = urllib.urlencode(data)
b64payload = b64encode(payload.encode())
sig = hmac.new(settings.DISCOURSE_SECRET_KEY.encode(), b64payload, sha256).hexdigest()
return HttpResponseRedirect(settings.DISCOURSE_BASE_URL + '/session/sso_provider?' + urllib.urlencode({'sso': b64payload, 'sig': sig}))
class ContinueDiscourseAuth(View):
http_method_names = ['get']
def get(self, request, *args, **kwargs):
# Where do we want to go once authentication is complete?
nextUrl = request.session.get('discourse_next', "/")
rawSig = request.GET['sig']
rawSSO = request.GET['sso']
payload = urllib.unquote(rawSSO)
computed_sig = hmac.new(
settings.DISCOURSE_SECRET_KEY.encode(),
payload.encode(),
sha256
).hexdigest()
successful = hmac.compare_digest(computed_sig, rawSig.encode())
if not successful: # The signature doesn't match, not legit
raise ValidationError("Signature does not match, data has been manipulated")
decodedPayload = urllib.unquote_plus(b64decode(urllib.unquote(payload)).decode())
data = dict(data.split("=") for data in decodedPayload.split('&'))
# Get the nonce that's been returned by discourse
returnedNonce = data['nonce']
try: # See if it's in the database
storedAttempt = AuthAttempt.objects.get_acceptable().get(nonce=returnedNonce)
except AuthAttempt.DoesNotExist: # If it's not, this attempt is not valid
raise ValidationError("Nonce does not exist in database")
# Delete the nonce from the database - don't allow it to be reused
storedAttempt.delete()
# While we're at it, delete all the other expired attempts
AuthAttempt.objects.purge_unacceptable()
# If we've got this far, the attempt is valid, so let's load user information
external_id = int(data['external_id'])
# See if the user is already linked to a django user
try:
userLink = DiscourseUserLink.objects.get(discourse_user_id=external_id)
except DiscourseUserLink.DoesNotExist:
return self.linkNewUser(request, data)
# Load the user
user = userLink.django_user
# Slightly hacky way to login user without calling authenticate()
user.backend = "%s.%s" % (ModelBackend.__module__, ModelBackend.__name__)
# Login the user
login(request, user)
return HttpResponseRedirect(nextUrl)
def linkNewUser(self, request, data):
# Great, let's save the new user info in the session
request.session['discourse_data'] = data
request.session['discourse_started_registration'] = time.time()
if request.user is not None:
return HttpResponseRedirect(reverse('associate-user'))
else:
return HttpResponseRedirect(reverse('new-user'))
class SocialRegisterForm(RegistrationForm):
def __init__(self, *args, **kwargs):
super(SocialRegisterForm, self).__init__(*args, **kwargs)
self.fields.pop('password1')
self.fields.pop('password2')
self.fields['email'].widget.attrs['readonly'] = True
def clean_email(self):
initial = getattr(self, 'initial', None)
if(initial['email'] != self.cleaned_data['email']):
raise ValidationError("You cannot change the email")
return initial['email']
class AssociateDiscourseUser(TemplateView):
template_name = "DiscourseAuth/associate_user.html"
@method_decorator(login_required) # Require user is logged in for associating their account
def dispatch(self, request, *args, **kwargs):
self.data = self.request.session.get('discourse_data', None)
timeStarted = self.request.session.get('discourse_started_registration', 0)
max_reg_time = 20 * 60 # Seconds
if timeStarted < (time.time() - max_reg_time):
raise PermissionDenied('The Discourse authentication has expired, please try again')
if self.data is None:
raise PermissionDenied('Discourse authentication data is not present in this session')
return super(AssociateDiscourseUser, self).dispatch(request, *args, **kwargs)
def get(self, request, **kwargs):
return super(AssociateDiscourseUser, self).get(self, request, **kwargs)
def get_context_data(self, *args, **kwargs):
c = super(AssociateDiscourseUser, self).get_context_data()
c['discourseuser'] = self.data['username']
c['djangouser'] = self.request.user.username
return c
def post(self, request, **kwargs):
DiscourseUserLink.objects.filter(Q(django_user=request.user) | Q(discourse_user_id=self.data['external_id'])).delete()
DiscourseUserLink.objects.create(django_user=request.user, discourse_user_id=self.data['external_id'])
messages.success(self.request, 'Accounts successfully linked, you are now logged in.')
# Redirect them to the discourse login URL
nextUrl = "{}?next={}".format(reverse('start-auth'), request.session.get('discourse_next', "/"))
return HttpResponseRedirect(nextUrl)
class NewDiscourseUser(FormView):
template_name = 'registration/registration_form.html'
def dispatch(self, request, *args, **kwargs):
self.data = self.request.session.get('discourse_data', None)
timeStarted = self.request.session.get('discourse_started_registration', 0)
max_reg_time = 20 * 60 # Seconds
if timeStarted < (time.time() - max_reg_time):
raise PermissionDenied('The Discourse authentication has expired, please try again')
if self.data is None:
raise PermissionDenied('Discourse authentication data is not present in this session')
return super(NewDiscourseUser, self).dispatch(request, *args, **kwargs)
def get_initial(self):
data = self.data
initialForm = {
'username': data['username'],
'email': data['email'],
'name': data['name']
}
return initialForm
def get_form_class(self):
if settings.DISCOURSE_REGISTRATION_FORM:
return settings.DISCOURSE_REGISTRATION_FORM
else:
return SocialRegisterForm
def form_valid(self, form):
# This method is called when valid form data has been POSTed.
user = get_user_model().objects.create_user(**form.cleaned_data)
# Link the user to Discourse account
DiscourseUserLink.objects.filter(discourse_user_id=self.data['external_id']).delete()
DiscourseUserLink.objects.create(django_user=user, discourse_user_id=self.data['external_id'])
messages.success(self.request, 'Account successfully created, you are now logged in.')
# Redirect them to the discourse login URL
nextUrl = "{}?next={}".format(reverse('start-auth'), self.request.session.get('discourse_next', "/"))
return HttpResponseRedirect(nextUrl)
class DisassociateDiscourseUser(TemplateView):
template_name = "DiscourseAuth/disassociate_user.html"
@method_decorator(login_required) # Require user is logged in for associating their account
def dispatch(self, request, *args, **kwargs):
return super(DisassociateDiscourseUser, self).dispatch(request, *args, **kwargs)
def get_context_data(self, *args, **kwargs):
c = super(DisassociateDiscourseUser, self).get_context_data()
links = DiscourseUserLink.objects.filter(django_user=self.request.user)
c['haslink'] = links.count() > 0
return c
def post(self, request, **kwargs):
DiscourseUserLink.objects.filter(django_user=request.user).delete()
return self.get(self, request, **kwargs)