Update community authentication to pass an arbitrary datablock instead of url
authorMagnus Hagander <magnus@hagander.net>
Thu, 20 Jun 2013 13:16:47 +0000 (15:16 +0200)
committerMagnus Hagander <magnus@hagander.net>
Thu, 20 Jun 2013 13:16:47 +0000 (15:16 +0200)
This makes it possible to pass URLs that will fail when they end up being double
escaped in some cases, since they contain non-url-safe characters. Instead, they'd
be base64-encoded, and thus safe.

Also update the django community auth provider to do just this, including encrypting
the data with the site secret key to make sure it can't be changed/injected by
tricking the user to go directly to the wrong URL.

docs/authentication.rst
pgweb/account/views.py
tools/communityauth/sample/django/auth.py

index 5829d883a13f0c983270781a38c5b4c4d8f46c6f..d3e5110d6169818f3cd93ef7990f075b608f2eb1 100644 (file)
@@ -53,10 +53,14 @@ The flow of an authentication in the 2.0 system is fairly simple:
    The <id> number in this URL is unique for each site, and is the
    identifier that accesses all encryption keys and redirection
    information.
-   In this call, the client site can optionally include a parameter
-   *su*, which will be used in the final redirection step. This URL
-   must start with a / to be considered, to prevent cross site
-   redirection.
+   In this call, the client can optionally include a parameter
+   *d*, which will be passed through back on the login confirmation.
+   This should be a base64 encoded parameter (other than the base64
+   character, the *$* character is also allowed and can be used to
+   split fields).
+   The client should encrypt or sign this parameter as necessary, and
+   without encryption/signature it should *not* be trusted, since it
+   can be injected into the authentication process without verification.
 #. The main website will check if the user holds a valid, logged in,
    session on the main website. If it does not, the user will be
    sent through the standard login path on the main website, and once
@@ -72,8 +76,10 @@ The flow of an authentication in the 2.0 system is fairly simple:
      The last name of the user logged in
    e
      The email address of the user logged in
+   d
+     base64 encoded data block to be passed along in confirmation (optional)
    su
-     The suburl to redirect to (optional)
+     *DEPRECATED* The suburl to redirect to (optional)
    t
      The timestamp of the authentication, in seconds-since-epoch. This
      should be validated against the current time, and authentication
@@ -110,7 +116,10 @@ The flow of an authentication in the 2.0 system is fairly simple:
    this is the case.
 #. The community site logs the user in using whatever method it's framework
    uses.
-#. If the *su* key is present in the data structure handed over, the
+#. If the *d* key is present in the data structure handed over, the
+   community site implements a site-specific action based on this data,
+   such as redirecting the user to the original location.
+#. *DEPRECATED* If the *su* key is present in the data structure handed over, the
    community site redirects to this location. If it's not present, then
    the community site will redirect so some default location on the
    site.
index cb3fc061e88a15005397ab88b993b54db6b137fe..421fcd1265321836fe08df36d9586551a6908407 100644 (file)
@@ -10,6 +10,7 @@ from django.conf import settings
 
 import base64
 import urllib
+import re
 from Crypto.Cipher import AES
 from Crypto import Random
 import time
@@ -219,6 +220,8 @@ def communityauth(request, siteid):
        # Get whatever site the user is trying to log in to.
        site = get_object_or_404(CommunityAuthSite, pk=siteid)
 
+       # "suburl" - old style way of passing parameters
+       # deprecated - will be removed once all sites have migrated
        if request.GET.has_key('su'):
                su = request.GET['su']
                if not su.startswith('/'):
@@ -226,18 +229,30 @@ def communityauth(request, siteid):
        else:
                su = None
 
+       # "data" - new style way of passing parameter, where we only
+       # care that it's characters are what's in base64.
+       if request.GET.has_key('d'):
+               d = request.GET['d']
+               if d != urllib.quote_plus(d, '=$'):
+                       # Invalid character, so drop it
+                       d = None
+       else:
+               d = None
+
        # Verify if the user is authenticated, and if he/she is not, generate
        # a login form that has information about which site is being logged
        # in to, and basic information about how the community login system
        # works.
        if not request.user.is_authenticated():
-               if su:
-                       suburl = "?su=%s" % su
+               if d:
+                       urldata = "?d=%s" % d
+               elif su:
+                       urldata = "?su=%s" % su
                else:
-                       suburl = ""
+                       urldata = ""
                return render_to_response('account/communityauth.html', {
                                'sitename': site.name,
-                               'next': '/account/auth/%s/%s' % (siteid, suburl),
+                               'next': '/account/auth/%s/%s' % (siteid, urldata),
                                }, NavContext(request, 'account'))
 
 
@@ -256,8 +271,10 @@ def communityauth(request, siteid):
                'l': request.user.last_name.encode('utf-8'),
                'e': request.user.email.encode('utf-8'),
                }
-       if su:
-               info['su'] = request.GET['su'].encode('utf-8')
+       if d:
+               info['d'] = d.encode('utf-8')
+       elif su:
+               info['su'] = d.encode('utf-8')
 
        # Turn this into an URL. Make sure the timestamp is always first, that makes
        # the first block more random..
index c3118d518d56c75e7f1693e2798bc87d353e9b85..3f750b4a5d23d15e3a46e1176956ded26e2fa40a 100644 (file)
@@ -27,8 +27,10 @@ from django.conf import settings
 
 import base64
 import urlparse
-from urllib import quote_plus
+import urllib
 from Crypto.Cipher import AES
+from Crypto.Hash import SHA
+from Crypto import Random
 import time
 
 class AuthBackend(ModelBackend):
@@ -45,9 +47,20 @@ class AuthBackend(ModelBackend):
 # Handle login requests by sending them off to the main site
 def login(request):
        if request.GET.has_key('next'):
-               return HttpResponseRedirect("%s?su=%s" % (
+               # Put together an url-encoded dict of parameters we're getting back,
+               # including a small nonce at the beginning to make sure it doesn't
+               # encrypt the same way every time.
+               s = "t=%s&%s" % (int(time.time()), urllib.urlencode({'r': request.GET['next']}))
+               # Now encrypt it
+               r = Random.new()
+               iv = r.read(16)
+               encryptor = AES.new(SHA.new(settings.SECRET_KEY).digest()[:16], AES.MODE_CBC, iv)
+               cipher = encryptor.encrypt(s + ' ' * (16-(len(s) % 16))) # pad to 16 bytes
+
+               return HttpResponseRedirect("%s?d=%s$%s" % (
                                settings.PGAUTH_REDIRECT,
-                               quote_plus(request.GET['next']),
+                           base64.b64encode(iv, "-_"),
+                           base64.b64encode(cipher, "-_"),
                                ))
        else:
                return HttpResponseRedirect(settings.PGAUTH_REDIRECT)
@@ -119,9 +132,21 @@ def auth_receive(request):
        user.backend = "%s.%s" % (AuthBackend.__module__, AuthBackend.__name__)
        django_login(request, user)
 
-       # Finally, redirect the user
-       if data.has_key('su'):
-               return HttpResponseRedirect(data['su'][0])
+       # Finally, check of we have a data package that tells us where to
+       # redirect the user.
+       if data.has_key('d'):
+               (ivs, datas) = data['d'][0].split('$')
+               decryptor = AES.new(SHA.new(settings.SECRET_KEY).digest()[:16],
+                                                       AES.MODE_CBC,
+                                                       base64.b64decode(ivs, "-_"))
+               s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(' ')
+               try:
+                       rdata = urlparse.parse_qs(s, strict_parsing=True)
+               except ValueError, e:
+                       raise Exception("Invalid encrypted data received.")
+               if rdata.has_key('r'):
+                       # Redirect address
+                       return HttpResponseRedirect(rdata['r'][0])
        # No redirect specified, see if we have it in our settings
        if hasattr(settings, 'PGAUTH_REDIRECT_SUCCESS'):
                return HttpResponseRedirect(settings.PGAUTH_REDIRECT_SUCCESS)