Last active
October 18, 2024 14:20
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Changed, thanks!
Thanks for sharing this. Was wondering if this will for Django 3.x? Any chance you have a CBV version? ๐
๐ @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
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