Skip to content

Commit 78de94d

Browse files
committed
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.
1 parent bd539a3 commit 78de94d

File tree

3 files changed

+69
-18
lines changed

3 files changed

+69
-18
lines changed

docs/authentication.rst

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,14 @@ The flow of an authentication in the 2.0 system is fairly simple:
5353
The <id> number in this URL is unique for each site, and is the
5454
identifier that accesses all encryption keys and redirection
5555
information.
56-
In this call, the client site can optionally include a parameter
57-
*su*, which will be used in the final redirection step. This URL
58-
must start with a / to be considered, to prevent cross site
59-
redirection.
56+
In this call, the client can optionally include a parameter
57+
*d*, which will be passed through back on the login confirmation.
58+
This should be a base64 encoded parameter (other than the base64
59+
character, the *$* character is also allowed and can be used to
60+
split fields).
61+
The client should encrypt or sign this parameter as necessary, and
62+
without encryption/signature it should *not* be trusted, since it
63+
can be injected into the authentication process without verification.
6064
#. The main website will check if the user holds a valid, logged in,
6165
session on the main website. If it does not, the user will be
6266
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:
7276
The last name of the user logged in
7377
e
7478
The email address of the user logged in
79+
d
80+
base64 encoded data block to be passed along in confirmation (optional)
7581
su
76-
The suburl to redirect to (optional)
82+
*DEPRECATED* The suburl to redirect to (optional)
7783
t
7884
The timestamp of the authentication, in seconds-since-epoch. This
7985
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:
110116
this is the case.
111117
#. The community site logs the user in using whatever method it's framework
112118
uses.
113-
#. If the *su* key is present in the data structure handed over, the
119+
#. If the *d* key is present in the data structure handed over, the
120+
community site implements a site-specific action based on this data,
121+
such as redirecting the user to the original location.
122+
#. *DEPRECATED* If the *su* key is present in the data structure handed over, the
114123
community site redirects to this location. If it's not present, then
115124
the community site will redirect so some default location on the
116125
site.

pgweb/account/views.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import base64
1212
import urllib
13+
import re
1314
from Crypto.Cipher import AES
1415
from Crypto import Random
1516
import time
@@ -219,25 +220,39 @@ def communityauth(request, siteid):
219220
# Get whatever site the user is trying to log in to.
220221
site = get_object_or_404(CommunityAuthSite, pk=siteid)
221222

223+
# "suburl" - old style way of passing parameters
224+
# deprecated - will be removed once all sites have migrated
222225
if request.GET.has_key('su'):
223226
su = request.GET['su']
224227
if not su.startswith('/'):
225228
su = None
226229
else:
227230
su = None
228231

232+
# "data" - new style way of passing parameter, where we only
233+
# care that it's characters are what's in base64.
234+
if request.GET.has_key('d'):
235+
d = request.GET['d']
236+
if d != urllib.quote_plus(d, '=$'):
237+
# Invalid character, so drop it
238+
d = None
239+
else:
240+
d = None
241+
229242
# Verify if the user is authenticated, and if he/she is not, generate
230243
# a login form that has information about which site is being logged
231244
# in to, and basic information about how the community login system
232245
# works.
233246
if not request.user.is_authenticated():
234-
if su:
235-
suburl = "?su=%s" % su
247+
if d:
248+
urldata = "?d=%s" % d
249+
elif su:
250+
urldata = "?su=%s" % su
236251
else:
237-
suburl = ""
252+
urldata = ""
238253
return render_to_response('account/communityauth.html', {
239254
'sitename': site.name,
240-
'next': '/account/auth/%s/%s' % (siteid, suburl),
255+
'next': '/account/auth/%s/%s' % (siteid, urldata),
241256
}, NavContext(request, 'account'))
242257

243258

@@ -256,8 +271,10 @@ def communityauth(request, siteid):
256271
'l': request.user.last_name.encode('utf-8'),
257272
'e': request.user.email.encode('utf-8'),
258273
}
259-
if su:
260-
info['su'] = request.GET['su'].encode('utf-8')
274+
if d:
275+
info['d'] = d.encode('utf-8')
276+
elif su:
277+
info['su'] = d.encode('utf-8')
261278

262279
# Turn this into an URL. Make sure the timestamp is always first, that makes
263280
# the first block more random..

tools/communityauth/sample/django/auth.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727

2828
import base64
2929
import urlparse
30-
from urllib import quote_plus
30+
import urllib
3131
from Crypto.Cipher import AES
32+
from Crypto.Hash import SHA
33+
from Crypto import Random
3234
import time
3335

3436
class AuthBackend(ModelBackend):
@@ -45,9 +47,20 @@ def authenticate(self, username=None, password=None):
4547
# Handle login requests by sending them off to the main site
4648
def login(request):
4749
if request.GET.has_key('next'):
48-
return HttpResponseRedirect("%s?su=%s" % (
50+
# Put together an url-encoded dict of parameters we're getting back,
51+
# including a small nonce at the beginning to make sure it doesn't
52+
# encrypt the same way every time.
53+
s = "t=%s&%s" % (int(time.time()), urllib.urlencode({'r': request.GET['next']}))
54+
# Now encrypt it
55+
r = Random.new()
56+
iv = r.read(16)
57+
encryptor = AES.new(SHA.new(settings.SECRET_KEY).digest()[:16], AES.MODE_CBC, iv)
58+
cipher = encryptor.encrypt(s + ' ' * (16-(len(s) % 16))) # pad to 16 bytes
59+
60+
return HttpResponseRedirect("%s?d=%s$%s" % (
4961
settings.PGAUTH_REDIRECT,
50-
quote_plus(request.GET['next']),
62+
base64.b64encode(iv, "-_"),
63+
base64.b64encode(cipher, "-_"),
5164
))
5265
else:
5366
return HttpResponseRedirect(settings.PGAUTH_REDIRECT)
@@ -119,9 +132,21 @@ def auth_receive(request):
119132
user.backend = "%s.%s" % (AuthBackend.__module__, AuthBackend.__name__)
120133
django_login(request, user)
121134

122-
# Finally, redirect the user
123-
if data.has_key('su'):
124-
return HttpResponseRedirect(data['su'][0])
135+
# Finally, check of we have a data package that tells us where to
136+
# redirect the user.
137+
if data.has_key('d'):
138+
(ivs, datas) = data['d'][0].split('$')
139+
decryptor = AES.new(SHA.new(settings.SECRET_KEY).digest()[:16],
140+
AES.MODE_CBC,
141+
base64.b64decode(ivs, "-_"))
142+
s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(' ')
143+
try:
144+
rdata = urlparse.parse_qs(s, strict_parsing=True)
145+
except ValueError, e:
146+
raise Exception("Invalid encrypted data received.")
147+
if rdata.has_key('r'):
148+
# Redirect address
149+
return HttpResponseRedirect(rdata['r'][0])
125150
# No redirect specified, see if we have it in our settings
126151
if hasattr(settings, 'PGAUTH_REDIRECT_SUCCESS'):
127152
return HttpResponseRedirect(settings.PGAUTH_REDIRECT_SUCCESS)

0 commit comments

Comments
 (0)