1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
|
#
# Django module to support postgresql.org community authentication 2.0
#
# The main location for this module is the pgweb git repository hosted
# on git.postgresql.org - look there for updates.
#
# To integrate with django, you need the following:
# * Make sure the view "login" from this module is used for login
# * Map an url somwehere (typically /auth_receive/) to the auth_receive
# view.
# * In settings.py, set AUTHENTICATION_BACKENDS to point to the class
# AuthBackend in this module.
# * (And of course, register for a crypto key with the main authentication
# provider website)
# * If the application uses the django admin interface, the login screen
# has to be replaced with something similar to login.html in this
# directory (adjust urls, and name it admin/login.html in any template
# directory that's processed before the default django.contrib.admin)
#
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.models import User
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import login as django_login
from django.contrib.auth import logout as django_logout
from django.conf import settings
import base64
import json
import socket
import urlparse
import urllib
from Crypto.Cipher import AES
from Crypto.Hash import SHA
from Crypto import Random
import time
class AuthBackend(ModelBackend):
# We declare a fake backend that always fails direct authentication -
# since we should never be using direct authentication in the first place!
def authenticate(self, username=None, password=None):
raise Exception("Direct authentication not supported")
####
# Two regular django views to interact with the login system
####
# Handle login requests by sending them off to the main site
def login(request):
if request.GET.has_key('next'):
# 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,
base64.b64encode(iv, "-_"),
base64.b64encode(cipher, "-_"),
))
else:
return HttpResponseRedirect(settings.PGAUTH_REDIRECT)
# Handle logout requests by logging out of this site and then
# redirecting to log out from the main site as well.
def logout(request):
if request.user.is_authenticated():
django_logout(request)
return HttpResponseRedirect("%slogout/" % settings.PGAUTH_REDIRECT)
# Receive an authentication response from the main website and try
# to log the user in.
def auth_receive(request):
if request.GET.has_key('s') and request.GET['s'] == "logout":
# This was a logout request
return HttpResponseRedirect('/')
if not request.GET.has_key('i'):
return HttpResponse("Missing IV in url!", status=400)
if not request.GET.has_key('d'):
return HttpResponse("Missing data in url!", status=400)
# Set up an AES object and decrypt the data we received
decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY),
AES.MODE_CBC,
base64.b64decode(str(request.GET['i']), "-_"))
s = decryptor.decrypt(base64.b64decode(str(request.GET['d']), "-_")).rstrip(' ')
# Now un-urlencode it
try:
data = urlparse.parse_qs(s, strict_parsing=True)
except ValueError:
return HttpResponse("Invalid encrypted data received.", status=400)
# Check the timestamp in the authentication
if (int(data['t'][0]) < time.time() - 10):
return HttpResponse("Authentication token too old.", status=400)
# Update the user record (if any)
try:
user = User.objects.get(username=data['u'][0])
# User found, let's see if any important fields have changed
changed = False
if user.first_name != data['f'][0]:
user.first_name = data['f'][0]
changed = True
if user.last_name != data['l'][0]:
user.last_name = data['l'][0]
changed = True
if user.email != data['e'][0]:
user.email = data['e'][0]
changed= True
if changed:
user.save()
except User.DoesNotExist:
# User not found, create it!
# NOTE! We have some legacy users where there is a user in
# the database with a different userid. Instead of trying to
# somehow fix that live, give a proper error message and
# have somebody look at it manually.
if User.objects.filter(email=data['e'][0]).exists():
return HttpResponse("""A user with email %s already exists, but with
a different username than %s.
This is almost certainly caused by some legacy data in our database.
Please send an email to [email protected], indicating the username
and email address from above, and we'll manually marge the two accounts
for you.
We apologize for the inconvenience.
""" % (data['e'][0], data['u'][0]), content_type='text/plain')
user = User(username=data['u'][0],
first_name=data['f'][0],
last_name=data['l'][0],
email=data['e'][0],
password='setbypluginnotasha1',
)
user.save()
# Ok, we have a proper user record. Now tell django that
# we're authenticated so it persists it in the session. Before
# we do that, we have to annotate it with the backend information.
user.backend = "%s.%s" % (AuthBackend.__module__, AuthBackend.__name__)
django_login(request, user)
# 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:
return HttpResponse("Invalid encrypted data received.", status=400)
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)
return HttpResponse("Authentication successful, but don't know where to redirect!", status=500)
# Perform a search in the central system. Note that the results are returned as an
# array of dicts, and *not* as User objects. To be able to for example reference the
# user through a ForeignKey, a User object must be materialized locally. We don't do
# that here, as this search might potentially return a lot of unrelated users since
# it's a wildcard match.
# Unlike the authentication, searching does not involve the browser - we just make
# a direct http call.
def user_search(searchterm=None, userid=None):
# If upsteam isn't responding quickly, it's not going to respond at all, and
# 10 seconds is already quite long.
socket.setdefaulttimeout(10)
if userid:
q = {'u': userid}
else:
q = {'s': searchterm}
u = urllib.urlopen('%ssearch/?%s' % (
settings.PGAUTH_REDIRECT,
urllib.urlencode(q),
))
(ivs, datas) = u.read().split('&')
u.close()
# Decryption time
decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY),
AES.MODE_CBC,
base64.b64decode(ivs, "-_"))
s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(' ')
j = json.loads(s)
return j
|