mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-01-25 01:12:16 +00:00
Proof of concept discourse authentication
This commit is contained in:
@@ -58,6 +58,7 @@ INSTALLED_APPS = (
|
|||||||
'captcha',
|
'captcha',
|
||||||
'widget_tweaks',
|
'widget_tweaks',
|
||||||
'raven.contrib.django.raven_compat',
|
'raven.contrib.django.raven_compat',
|
||||||
|
'social.apps.django_app.default',
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
MIDDLEWARE_CLASSES = (
|
||||||
@@ -73,6 +74,63 @@ MIDDLEWARE_CLASSES = (
|
|||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'RIGS.discourse.discourse.DiscourseAuth',
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Environ
|
||||||
|
# DISCOURSE_HOST='http://localhost:4000'
|
||||||
|
# DISCOURSE_SSO_SECRET='ABCDEFGHIJKLMNOP'
|
||||||
|
|
||||||
|
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/rigboard'
|
||||||
|
SOCIAL_AUTH_LOGIN_URL = '/'
|
||||||
|
|
||||||
|
SOCIAL_AUTH_PIPELINE = (
|
||||||
|
# Get the information we can about the user and return it in a simple
|
||||||
|
# format to create the user instance later. On some cases the details are
|
||||||
|
# already part of the auth response from the provider, but sometimes this
|
||||||
|
# could hit a provider API.
|
||||||
|
'social.pipeline.social_auth.social_details',
|
||||||
|
|
||||||
|
# Get the social uid from whichever service we're authing thru. The uid is
|
||||||
|
# the unique identifier of the given user in the provider.
|
||||||
|
'social.pipeline.social_auth.social_uid',
|
||||||
|
|
||||||
|
# Verifies that the current auth process is valid within the current
|
||||||
|
# project, this is were emails and domains whitelists are applied (if
|
||||||
|
# defined).
|
||||||
|
'social.pipeline.social_auth.auth_allowed',
|
||||||
|
|
||||||
|
# Checks if the current social-account is already associated in the site.
|
||||||
|
'social.pipeline.social_auth.social_user',
|
||||||
|
|
||||||
|
# Make up a username for this person, appends a random string at the end if
|
||||||
|
# there's any collision.
|
||||||
|
# 'social.pipeline.user.get_username',
|
||||||
|
|
||||||
|
# Send a validation email to the user to verify its email address.
|
||||||
|
# Disabled by default.
|
||||||
|
# 'social.pipeline.mail.mail_validation',
|
||||||
|
|
||||||
|
# Associates the current social details with another user account with
|
||||||
|
# a similar email address. Disabled by default.
|
||||||
|
'social.pipeline.social_auth.associate_by_email',
|
||||||
|
|
||||||
|
# Create a user account if we haven't found one yet.
|
||||||
|
#'social.pipeline.user.create_user',
|
||||||
|
|
||||||
|
# Create the record that associated the social account with this user.
|
||||||
|
#'social.pipeline.social_auth.associate_user',
|
||||||
|
|
||||||
|
# Populate the extra_data field in the social record with the values
|
||||||
|
# specified by settings (and the default ones like access_token, etc).
|
||||||
|
'social.pipeline.social_auth.load_extra_data',
|
||||||
|
|
||||||
|
# Update the user record with any changed info from the auth service.
|
||||||
|
'social.pipeline.user.user_details',
|
||||||
|
)
|
||||||
|
|
||||||
ROOT_URLCONF = 'PyRIGS.urls'
|
ROOT_URLCONF = 'PyRIGS.urls'
|
||||||
|
|
||||||
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
||||||
@@ -202,6 +260,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
|||||||
"django.core.context_processors.tz",
|
"django.core.context_processors.tz",
|
||||||
"django.core.context_processors.request",
|
"django.core.context_processors.request",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
'social.apps.django_app.context_processors.backends',
|
||||||
|
'social.apps.django_app.context_processors.login_redirect',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ urlpatterns = patterns('',
|
|||||||
url('^user/', include('registration.backends.default.urls')),
|
url('^user/', include('registration.backends.default.urls')),
|
||||||
|
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
|
url('', include('social.apps.django_app.urls', namespace='social'))
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
0
RIGS/discourse/__init__.py
Normal file
0
RIGS/discourse/__init__.py
Normal file
89
RIGS/discourse/discourse.py
Normal file
89
RIGS/discourse/discourse.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import os
|
||||||
|
from social.backends.base import BaseAuth
|
||||||
|
from social.exceptions import AuthException
|
||||||
|
|
||||||
|
from .sso import DiscourseSSO
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseAssociation(object):
|
||||||
|
""" Use Association model to save the nonce by force. """
|
||||||
|
|
||||||
|
def __init__(self, handle, secret='', issued=0, lifetime=0, assoc_type=''):
|
||||||
|
self.handle = handle # as nonce
|
||||||
|
self.secret = secret.encode() # not use
|
||||||
|
self.issued = issued # not use
|
||||||
|
self.lifetime = lifetime # not use
|
||||||
|
self.assoc_type = assoc_type # as state
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseAuth(BaseAuth):
|
||||||
|
"""Discourse authentication backend"""
|
||||||
|
name = 'discourse'
|
||||||
|
secret = os.environ['DISCOURSE_SSO_SECRET']
|
||||||
|
host = os.environ['DISCOURSE_HOST']
|
||||||
|
|
||||||
|
EXTRA_DATA = [
|
||||||
|
('username', 'username'),
|
||||||
|
('email', 'email'),
|
||||||
|
('external_id', 'external_id')
|
||||||
|
]
|
||||||
|
|
||||||
|
sso = DiscourseSSO(secret)
|
||||||
|
|
||||||
|
def get_and_store_nonce(self, url):
|
||||||
|
# Create a nonce
|
||||||
|
nonce = self.strategy.random_string(64)
|
||||||
|
# Store the nonce
|
||||||
|
association = DiscourseAssociation(nonce)
|
||||||
|
self.strategy.storage.association.store(url, association)
|
||||||
|
return nonce
|
||||||
|
|
||||||
|
def get_nonce(self, nonce):
|
||||||
|
try:
|
||||||
|
return self.strategy.storage.association.get(
|
||||||
|
server_url=self.host,
|
||||||
|
handle=nonce
|
||||||
|
)[0]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_nonce(self, nonce_id):
|
||||||
|
self.strategy.storage.association.remove([nonce_id])
|
||||||
|
|
||||||
|
def get_user_id(self, details, response):
|
||||||
|
"""Return current user id."""
|
||||||
|
|
||||||
|
return int(response['external_id'])
|
||||||
|
|
||||||
|
def get_user_details(self, response):
|
||||||
|
"""Return user basic information (id and email only)."""
|
||||||
|
|
||||||
|
return {'username': response['username'],
|
||||||
|
'email': response['email'],
|
||||||
|
'fullname': response['name'].replace('+', ' ') if 'name' in response else '',
|
||||||
|
'first_name': '',
|
||||||
|
'last_name': ''}
|
||||||
|
|
||||||
|
def auth_url(self):
|
||||||
|
"""Build and return complete URL."""
|
||||||
|
nonce = self.get_and_store_nonce(self.host)
|
||||||
|
|
||||||
|
return self.host + self.sso.build_login_URL(nonce, self.redirect_uri)
|
||||||
|
|
||||||
|
def auth_complete(self, *args, **kwargs):
|
||||||
|
"""Completes login process, must return user instance."""
|
||||||
|
|
||||||
|
if not self.sso.validate(self.data['sso'], self.data['sig']):
|
||||||
|
raise Exception("Someone wants to hack us!")
|
||||||
|
|
||||||
|
nonce = self.sso.get_nonce(self.data['sso'])
|
||||||
|
nonce_obj = self.get_nonce(nonce)
|
||||||
|
if nonce_obj:
|
||||||
|
self.remove_nonce(nonce_obj.id)
|
||||||
|
else:
|
||||||
|
raise Exception("Nonce does not match!")
|
||||||
|
|
||||||
|
kwargs.update({'response': self.sso.get_data(
|
||||||
|
self.data['sso']), 'backend': self})
|
||||||
|
return self.strategy.authenticate(*args, **kwargs)
|
||||||
46
RIGS/discourse/sso.py
Normal file
46
RIGS/discourse/sso.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import urllib
|
||||||
|
from hashlib import sha256
|
||||||
|
import hmac
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseSSO:
|
||||||
|
def __init__(self, secret_key):
|
||||||
|
self.__secret_key = secret_key
|
||||||
|
|
||||||
|
def validate(self, payload, sig):
|
||||||
|
payload = urllib.unquote(payload)
|
||||||
|
computed_sig = hmac.new(
|
||||||
|
self.__secret_key.encode(),
|
||||||
|
payload.encode(),
|
||||||
|
sha256
|
||||||
|
).hexdigest()
|
||||||
|
print(type(computed_sig), type(sig))
|
||||||
|
return hmac.compare_digest(unicode(computed_sig), sig)
|
||||||
|
|
||||||
|
def get_nonce(self, payload):
|
||||||
|
payload = b64decode(urllib.unquote(payload)).decode()
|
||||||
|
d = dict(nonce.split("=") for nonce in payload.split('&'))
|
||||||
|
|
||||||
|
if 'nonce' in d and d['nonce'] != '':
|
||||||
|
return d['nonce']
|
||||||
|
else:
|
||||||
|
raise Exception("Nonce could not be found in payload")
|
||||||
|
|
||||||
|
def get_data(self, payload):
|
||||||
|
payload = urllib.unquote(b64decode(urllib.unquote(payload)).decode())
|
||||||
|
d = dict(data.split("=") for data in payload.split('&'))
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
def build_login_URL(self, nonce, redirect_uri):
|
||||||
|
data = {
|
||||||
|
'nonce': nonce,
|
||||||
|
'return_sso_url': redirect_uri
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = urllib.urlencode(data)
|
||||||
|
payload = b64encode(payload.encode())
|
||||||
|
sig = hmac.new(self.__secret_key.encode(), payload, sha256).hexdigest()
|
||||||
|
|
||||||
|
return '/session/sso_provider?' + urllib.urlencode({'sso': payload, 'sig': sig})
|
||||||
@@ -12,17 +12,23 @@ django-widget-tweaks==1.3
|
|||||||
gunicorn==19.3.0
|
gunicorn==19.3.0
|
||||||
icalendar==3.9.0
|
icalendar==3.9.0
|
||||||
lxml==3.4.4
|
lxml==3.4.4
|
||||||
|
oauthlib==2.0.0
|
||||||
Pillow==2.8.1
|
Pillow==2.8.1
|
||||||
psycopg2==2.6
|
psycopg2==2.6
|
||||||
Pygments==2.0.2
|
Pygments==2.0.2
|
||||||
|
PyJWT==1.4.2
|
||||||
PyPDF2==1.24
|
PyPDF2==1.24
|
||||||
python-dateutil==2.4.2
|
python-dateutil==2.4.2
|
||||||
|
python-openid==2.2.5
|
||||||
|
python-social-auth==0.2.21
|
||||||
pytz==2015.4
|
pytz==2015.4
|
||||||
raven==5.8.1
|
raven==5.8.1
|
||||||
reportlab==3.1.44
|
reportlab==3.1.44
|
||||||
|
requests==2.11.1
|
||||||
|
requests-oauthlib==0.7.0
|
||||||
selenium==2.53.6
|
selenium==2.53.6
|
||||||
simplejson==3.7.2
|
simplejson==3.7.2
|
||||||
six==1.9.0
|
six==1.10.0
|
||||||
sqlparse==0.1.15
|
sqlparse==0.1.15
|
||||||
static3==0.6.1
|
static3==0.6.1
|
||||||
svg2rlg==0.3
|
svg2rlg==0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user