[FEATURE] DirectSSO package for typo3.org 70/41570/4
authorXavier Perseguers <xavier@typo3.org>
Fri, 17 Jul 2015 10:09:39 +0000 (12:09 +0200)
committerXavier Perseguers <xavier@typo3.org>
Fri, 17 Jul 2015 22:29:07 +0000 (00:29 +0200)
Change-Id: I646152d1b94d7f83d258a553c33b3828d7943f6e
Reviewed-on: http://review.typo3.org/41570
Reviewed-by: Xavier Perseguers <xavier@typo3.org>
Tested-by: Xavier Perseguers <xavier@typo3.org>
typo3/files/directsso/INSTALL.rst [new file with mode: 0644]
typo3/files/directsso/__init__.py [new file with mode: 0644]
typo3/files/directsso/auth.py [new file with mode: 0644]
typo3/files/directsso/models.py [new file with mode: 0644]
typo3/files/directsso/settings.py [new file with mode: 0644]
typo3/files/directsso/tests.py [new file with mode: 0644]
typo3/files/directsso/urls.py [new file with mode: 0644]
typo3/files/directsso/views.py [new file with mode: 0644]
typo3/tasks/main.yml

diff --git a/typo3/files/directsso/INSTALL.rst b/typo3/files/directsso/INSTALL.rst
new file mode 100644 (file)
index 0000000..716950d
--- /dev/null
@@ -0,0 +1,34 @@
+Install the DirectSSO authentication back-end
+---------------------------------------------
+
+0. Install the requirements::
+
+       pip install m2crypto
+
+   or:
+
+       easy_install m2crypto
+
+1. Place the ``directsso`` folder somewhere in the Python path (e.g., ``external_apps``)
+2. Add an URLconf entry in :file:`pootle/urls.py`::
+
+       url('^auth/', include('directsso.urls')),
+
+   Note: the ``/auth/`` prefix can be anything you want, but it has to match the path path configured on the SSO server.
+
+   Note: the URLconf line should be high in the list, e.g. right after the Django admin lines.
+
+3. Add the ``directsso.auth.DirectSSOAuthBackend`` back-end to ``AUTHENTICATION_BACKENDS`` in :file:`localsettings.py`, e.g.::
+
+       AUTHENTICATION_BACKENDS = (
+           'django.contrib.auth.backends.ModelBackend',
+           'directsso.auth.DirectSSOAuthBackend',
+       )
+
+4. At the end of :file:`localsettings.py` import the directsso settings::
+
+       from directsso.settings import *
+
+5. Edit :file:`directsso/settings.py` to suit your needs.
+
+6. Restart your server to take the changes into account
diff --git a/typo3/files/directsso/__init__.py b/typo3/files/directsso/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/typo3/files/directsso/auth.py b/typo3/files/directsso/auth.py
new file mode 100644 (file)
index 0000000..35f7377
--- /dev/null
@@ -0,0 +1,22 @@
+from django.contrib.auth.models import User
+
+class DirectSSOAuthBackend:
+    def authenticate(self, direct_sso_username=None):
+        if not direct_sso_username:
+            return None
+        try:
+            user = User.objects.get(username=direct_sso_username)
+            # we only want to authenticate SSO users,
+            # not local, valid users (i.e. admins)
+            if user.has_usable_password():
+                return None
+
+            return user
+        except:
+            return None
+
+    def get_user(self, user_id):
+        try:
+            return User.objects.get(pk=user_id)
+        except User.DoesNotExist:
+            return None
diff --git a/typo3/files/directsso/models.py b/typo3/files/directsso/models.py
new file mode 100644 (file)
index 0000000..71a8362
--- /dev/null
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/typo3/files/directsso/settings.py b/typo3/files/directsso/settings.py
new file mode 100644 (file)
index 0000000..48195b9
--- /dev/null
@@ -0,0 +1,34 @@
+import os
+
+# The login URL
+DIRECT_SSO_LOGIN_URL = 'http://typo3.org/login/translation'
+
+# Technical contact email, this should be an admin on the local server
+DIRECT_SSO_CONTACT_EMAIL = 'xavier@typo3.org'
+
+# Path to the file or ASCII content of the key.
+DIRECT_SSO_PUBLIC_KEY = """
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu+x5xKoCGxc/I4jB5soN
+IqklkDH/jvLER0enJd1jIeOJ8jOjo7kMkMnt/5qfpIb4tnCYVLXR7OrhQ16TQfty
+wuaeeVothdthc3SWFCB9j3k7983RJOgHsQzFNLpGzokMbI0PIs4xZVEimegS3ItR
+1xWINdRkK/R6Y4oCx/SgR1hO5uqnYJNOytHRAe4JxKwNN2LSuvkNob15dHqTKU9M
+CwztPTtoHK2KwkgGHq6lLFpGLeKBa7r98Mm1TXV+VsD2M1sT1vgJ6ocXEdf153xY
+1zQmUecexTlS/oQnHGdtSWvNdPQBnfD6qADiC9YZwA15WHG2fEqsLuxiVVfIURNo
+IXqKpVuS96a3vkAqi/AECd+FzlIgeTOFLf+SZmRhsxECXesdoc2ZL8exBKK05Wqt
+YcnIuXhTsiqwDmsr9hhpaXHDFI/7aZFaV0C+Is3z67m/vfK8IRAVv9J5tAyIwqKz
+JV75+Wj/Uww0AqulHXm8Ayf+yS7MHwePDA/Sh2cE7Yv6unta4o3D/vhZg0QQlDKJ
+IZtldY+0HbgHJqEKxfipcA7gO1GpRlj6HVnwjHwJfcFfRwKdnDy+tWziUJQL50TV
+pJvAzTUnrt7k1xomc87kW7CFk58I6f1GH5PhDJnxhPM0i0nSRaLfsDsHv7k310co
+5Vpe3Ym6JZF7Nm/cLr/OAfsCAwEAAQ==
+-----END PUBLIC KEY-----
+"""
+
+# Configured on the Typo3 server
+DIRECT_SSO_TPA_ID = "Pootle Server"
+
+# Must be True on production: prevents the same URL to be reused to re-authenticate a user
+DIRECT_SSO_ENFORCE_NO_REPEAT_ATTACK = True
+
+# Redirect to this URL after a successfull login
+DIRECT_SSO_SUCCESS_REDIRECT_URL = '/accounts/personal/edit/'
diff --git a/typo3/files/directsso/tests.py b/typo3/files/directsso/tests.py
new file mode 100644 (file)
index 0000000..2247054
--- /dev/null
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/typo3/files/directsso/urls.py b/typo3/files/directsso/urls.py
new file mode 100644 (file)
index 0000000..f886032
--- /dev/null
@@ -0,0 +1,5 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('directsso.views',
+    url(r'^$', 'handle_auth', name="sso-handle-auth"),
+)
diff --git a/typo3/files/directsso/views.py b/typo3/files/directsso/views.py
new file mode 100644 (file)
index 0000000..00b2fc2
--- /dev/null
@@ -0,0 +1,112 @@
+from M2Crypto import BIO, RSA, EVP
+from django.conf import settings
+from django.contrib.auth import login, authenticate
+from django.contrib.auth.models import User
+from django.core.cache import cache
+from django.http import HttpResponseRedirect
+from django.utils.translation import ugettext_lazy as _
+import os, datetime, base64, hashlib
+
+
+def handle_auth(request):
+    """
+    Main entry point of the Direct SSO agent.
+    """
+
+    def verify_sig(payload, signature):
+        "Verify the signature digest using the configured public key"
+
+        # Load the public key
+        if os.path.exists(settings.DIRECT_SSO_PUBLIC_KEY):
+            ascii_pub_key = open(settings.DIRECT_SSO_PUBLIC_KEY,'r').read()
+        else:
+            ascii_pub_key = settings.DIRECT_SSO_PUBLIC_KEY
+
+        bio = BIO.MemoryBuffer(ascii_pub_key)
+        rsa = RSA.load_pub_key_bio(bio)
+        pubkey = EVP.PKey()
+        pubkey.assign_rsa(rsa)
+        pubkey.reset_context(md='sha1')
+        pubkey.verify_init()
+        pubkey.verify_update(payload)
+        return pubkey.verify_final(signature) == 1
+
+    # Calculate the hash of the querystring, check if it was
+    # used before
+    sso_link_hash = hashlib.new('sha1', request.META.get('QUERY_STRING')).hexdigest()
+    cache_key = 'sso_hash_%s' %sso_link_hash
+    if cache.get(cache_key) is None:
+        cache.set(cache_key,True, 3600)
+    else:
+        if settings.DIRECT_SSO_ENFORCE_NO_REPEAT_ATTACK:
+            raise Exception(_('This single-signon query string was used before. Please try the login process again. If the problem persists, please contact %s.') %settings.DIRECT_SSO_CONTACT_EMAIL)
+
+    # Sanitize all passed data
+    version, user, tpa_id, expires, action, flags, userdata, signature = \
+        request.GET.get('version',''), \
+        request.GET.get('user',''), \
+        request.GET.get('tpa_id',''), \
+        request.GET.get('expires',''), \
+        request.GET.get('action',''), \
+        request.GET.get('flags',''), \
+        request.GET.get('userdata',''), \
+        request.GET.get('signature','').decode('hex')
+
+    # TPA ID must match the configured one, just in case.
+    if tpa_id != settings.DIRECT_SSO_TPA_ID:
+        raise Exception(_('An invalid Application ID key was used. Please report this problem to %s.') %settings.DIRECT_SSO_CONTACT_EMAIL)
+
+    # Verify the signature digest
+    payload = 'version=%s&user=%s&tpa_id=%s&expires=%s&action=%s&flags=%s&userdata=%s' %(
+        version, user, tpa_id, expires, action, flags, userdata
+    )
+    payload = str(payload)
+    if not verify_sig(payload, signature):
+        raise Exception(_('Could not validate the SSO signature. Please report this problem to %s.') %settings.DIRECT_SSO_CONTACT_EMAIL)
+
+    # Make sure the URL is still valid
+    if datetime.datetime.fromtimestamp(float(expires)) < datetime.datetime.now():
+        raise Exception(_('The given single-signon URL has expired. Please try the login process again, if the problem persists, please contact %s.') %settings.DIRECT_SSO_CONTACT_EMAIL)
+
+    # All good, extract the userdata, stash it away in a dict
+    try:
+        udata = dict()
+        for pair in base64.b64decode(userdata).split('|'):
+            try:
+                k,v = pair.split('=')
+                udata[k] = v
+            except:
+                pass
+
+        userdata = udata
+    except:
+        raise Exception(_('The given userdata is invalid. Please try the login process again, if the problem persists, please contact %s.') %settings.DIRECT_SSO_CONTACT_EMAIL)
+
+    # Check whether the user exists, if not, create him
+    try:
+        user = User.objects.get(username=userdata.get('username'))
+    except User.DoesNotExist:
+        user = User.objects.create_user(
+            userdata.get('username'),
+            userdata.get('email', None)
+        )
+        user.is_active = True
+        user.set_unusable_password()
+
+    # Update the user data
+    if ' ' in userdata.get('name'):
+        user.first_name, user.last_name = userdata.get('name').rsplit(' ',1)
+    else:
+        user.first_name = userdata.get('name')
+    user.email = userdata.get('email', '')
+    user.save()
+
+    # Log 'em in
+    user = authenticate(direct_sso_username=userdata.get('username'))
+    if user is not None and user.is_active:
+        login(request, user)
+    else:
+        raise Exception(_('Could not authenticate you, sorry. Please contact %s') %settings.DIRECT_SSO_CONTACT_EMAIL)
+
+    # Success, redirect to the configured URL
+    return HttpResponseRedirect(settings.DIRECT_SSO_SUCCESS_REDIRECT_URL)
index 9b418b2..d8c9d61 100644 (file)
@@ -2,6 +2,18 @@
 - name: Create web directory for localisation packages
   file: dest=/var/www/{{ domain }}/public/l10n_ter mode=755 state=directory owner=pootle group=www-data recurse=yes
 
+- name: Install swig (directSSO)
+  apt: pkg=swig state=installed
+
+- name: Install libssl-dev (directSSO)
+  apt: pkg=libssl-dev state=installed
+
+- name: Install m2crypto for Python (directSSO)
+  pip: name=m2crypto virtualenv={{ pootle_virtualenv }}
+
+- name: Install directSSO
+  copy: src=directsso dest={{ pootle_virtualenv }}/lib/python2.7/site-packages
+
 - name: Install git
   apt: pkg=git state=installed update_cache=true