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.
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
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
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.
import base64
import urllib
+import re
from Crypto.Cipher import AES
from Crypto import Random
import time
# 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('/'):
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'))
'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..
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):
# 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)
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)