Skip to content

Instantly share code, notes, and snippets.

@alee
Last active March 24, 2024 19:00
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save alee/3c6161809ef78966454e434a8ed350d1 to your computer and use it in GitHub Desktop.
Save alee/3c6161809ef78966454e434a8ed350d1 to your computer and use it in GitHub Desktop.
example Django view function that can be used for Discourse SSO, i.e., Discourse delegates User authentication to Django
import base64
import hmac
import hashlib
from urllib import parse
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.conf import settings
@login_required
def discourse_sso(request):
'''
Django view function - register with `urls.py` and use that registered URL as the callback in your Discourse configuration.
Make sure to set `DISCOURSE_BASE_URL` and `DISCOURSE_SSO_SECRET` in settings.py
Code was originally adapted from https://meta.discourse.org/t/sso-example-for-django/14258 but that discussion topic appears to have been removed.
'''
payload = request.GET.get('sso')
signature = request.GET.get('sig')
if None in [payload, signature]:
return HttpResponseBadRequest('No SSO payload or signature. Please contact support if this problem persists.')
# Validate the payload
payload = bytes(parse.unquote(payload), encoding='utf-8')
decoded = base64.decodebytes(payload).decode('utf-8')
if len(payload) == 0 or 'nonce' not in decoded:
return HttpResponseBadRequest('Invalid payload. Please contact support if this problem persists.')
key = bytes(settings.DISCOURSE_SSO_SECRET, encoding='utf-8') # must not be unicode
h = hmac.new(key, payload, digestmod=hashlib.sha256)
this_signature = h.hexdigest()
if not hmac.compare_digest(this_signature, signature):
return HttpResponseBadRequest('Invalid payload. Please contact support if this problem persists.')
# Build the return payload
qs = parse.parse_qs(decoded)
user = request.user
params = {
'nonce': qs['nonce'][0],
'email': user.email,
'external_id': user.id,
'username': user.username,
'require_activation': 'true',
'name': user.get_full_name(),
}
return_payload = base64.encodebytes(bytes(parse.urlencode(params), 'utf-8'))
h = hmac.new(key, return_payload, digestmod=hashlib.sha256)
query_string = parse.urlencode({'sso': return_payload, 'sig': h.hexdigest()})
# Redirect back to Discourse
discourse_sso_url = f'{settings.DISCOURSE_BASE_URL}/session/sso_login?{query_string}'
return HttpResponseRedirect(discourse_sso_url)
@nilox94
Copy link

nilox94 commented Mar 2, 2018

I think it should say
- depends on settings.py DISCOURSE_BASE_URL and DISCOURSE_SSO_SECRET
instead of
- depends on settings.py BASE_DISCOURSE_URL and DISCOURSE_SSO_SECRET

@alee
Copy link
Author

alee commented Apr 13, 2018

Changed, thanks!

@shawnngtq
Copy link

@alee

Thanks for sharing this. Was wondering if this will for Django 3.x? Any chance you have a CBV version? 😄

@alee
Copy link
Author

alee commented Nov 15, 2021

👋 @shawnngtq

This works just fine for Django 3.x, AFAIK and it should be fairly straightforward to build a CBV version based on Django's RedirectView - I think you could put most of the logic in this method into get_redirect_url and replace references to the request parameter to self.request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment