mirror of
https://github.com/postgres/pgweb.git
synced 2025-07-29 11:59:36 +00:00
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:
@ -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.
|
||||
|
@ -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..
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user