Update community authentication to pass an arbitrary datablock instead of url

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.
This commit is contained in:
Magnus Hagander
2013-06-20 15:16:47 +02:00
parent bd539a392e
commit 78de94d17c
3 changed files with 69 additions and 18 deletions

View 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.

View 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..

View 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)