1 | #!/usr/bin/env python
|
---|
2 | # coding: utf8
|
---|
3 |
|
---|
4 | """
|
---|
5 | OpenID authentication for web2py
|
---|
6 |
|
---|
7 | Allowed using OpenID login together with web2py built-in login.
|
---|
8 |
|
---|
9 | By default, to support OpenID login, put this in your db.py
|
---|
10 |
|
---|
11 | >>> from gluon.contrib.login_methods.openid_auth import OpenIDAuth
|
---|
12 | >>> auth.settings.login_form = OpenIDAuth(auth)
|
---|
13 |
|
---|
14 | To show OpenID list in user profile, you can add the following code
|
---|
15 | before the end of function user() of your_app/controllers/default.py
|
---|
16 |
|
---|
17 | + if (request.args and request.args(0) == "profile"):
|
---|
18 | + form = DIV(form, openid_login_form.list_user_openids())
|
---|
19 | return dict(form=form, login_form=login_form, register_form=register_form, self_registration=self_registration)
|
---|
20 |
|
---|
21 | More detail in the description of the class OpenIDAuth.
|
---|
22 |
|
---|
23 | Requirements:
|
---|
24 | python-openid version 2.2.5 or later
|
---|
25 |
|
---|
26 | Reference:
|
---|
27 | * w2p openID
|
---|
28 | http://w2popenid.appspot.com/init/default/wiki/w2popenid
|
---|
29 | * RPX and web2py auth module
|
---|
30 | http://www.web2pyslices.com/main/slices/take_slice/28
|
---|
31 | * built-in file: gluon/contrib/login_methods/rpx_account.py
|
---|
32 | * built-in file: gluon/tools.py (Auth class)
|
---|
33 | """
|
---|
34 | import time
|
---|
35 | from datetime import datetime, timedelta
|
---|
36 |
|
---|
37 | from gluon.html import *
|
---|
38 | from gluon.http import redirect
|
---|
39 | from gluon.storage import Storage, Messages
|
---|
40 | from gluon.sql import Field, SQLField
|
---|
41 | from gluon.validators import IS_NOT_EMPTY, IS_NOT_IN_DB
|
---|
42 |
|
---|
43 | try:
|
---|
44 | import openid.consumer.consumer
|
---|
45 | from openid.association import Association
|
---|
46 | from openid.store.interface import OpenIDStore
|
---|
47 | from openid.extensions.sreg import SRegRequest, SRegResponse
|
---|
48 | from openid.store import nonce
|
---|
49 | from openid.consumer.discover import DiscoveryFailure
|
---|
50 | except ImportError, err:
|
---|
51 | raise ImportError("OpenIDAuth requires python-openid package")
|
---|
52 |
|
---|
53 | DEFAULT = lambda: None
|
---|
54 |
|
---|
55 | class OpenIDAuth(object):
|
---|
56 | """
|
---|
57 | OpenIDAuth
|
---|
58 |
|
---|
59 | It supports the logout_url, implementing the get_user and login_form
|
---|
60 | for cas usage of gluon.tools.Auth.
|
---|
61 |
|
---|
62 | It also uses the ExtendedLoginForm to allow the OpenIDAuth login_methods
|
---|
63 | combined with the standard logon/register procedure.
|
---|
64 |
|
---|
65 | It uses OpenID Consumer when render the form and begins the OpenID
|
---|
66 | authentication.
|
---|
67 |
|
---|
68 | Example: (put these code after auth.define_tables() in your models.)
|
---|
69 |
|
---|
70 | auth = Auth(globals(), db) # authentication/authorization
|
---|
71 | ...
|
---|
72 | auth.define_tables() # creates all needed tables
|
---|
73 | ...
|
---|
74 |
|
---|
75 | #include in your model after auth has been defined
|
---|
76 | from gluon.contrib.login_methods.openid_auth import OpenIDAuth
|
---|
77 | openid_login_form = OpenIDAuth(request, auth, db)
|
---|
78 |
|
---|
79 | from gluon.contrib.login_methods.extended_login_form import ExtendedLoginForm
|
---|
80 | extended_login_form = ExtendedLoginForm(request, auth, openid_login_form,
|
---|
81 | signals=['oid','janrain_nonce'])
|
---|
82 |
|
---|
83 | auth.settings.login_form = extended_login_form
|
---|
84 | """
|
---|
85 |
|
---|
86 | def __init__(self, auth):
|
---|
87 | self.auth = auth
|
---|
88 | self.db = auth.db
|
---|
89 | self.environment = auth.environment
|
---|
90 |
|
---|
91 | request = self.environment.request
|
---|
92 | self.nextvar = '_next'
|
---|
93 | self.realm = 'http://%s' % request.env.http_host
|
---|
94 | self.login_url = auth.environment.URL(r=request, f='user', args=['login'])
|
---|
95 | self.return_to_url = self.realm + self.login_url
|
---|
96 |
|
---|
97 | self.table_alt_logins_name = "alt_logins"
|
---|
98 | if not auth.settings.table_user:
|
---|
99 | raise
|
---|
100 | self.table_user = self.auth.settings.table_user
|
---|
101 | self.openid_expiration = 15 #minutes
|
---|
102 |
|
---|
103 | self.messages = self._define_messages()
|
---|
104 |
|
---|
105 | if not self.table_alt_logins_name in self.db.tables:
|
---|
106 | self._define_alt_login_table()
|
---|
107 |
|
---|
108 | def _define_messages(self):
|
---|
109 | messages = Messages(self.environment.T)
|
---|
110 | messages.label_alt_login_username = 'Sign-in with OpenID: '
|
---|
111 | messages.label_add_alt_login_username = 'Add a new OpenID: '
|
---|
112 | messages.submit_button = 'Sign in'
|
---|
113 | messages.submit_button_add = 'Add'
|
---|
114 | messages.a_delete = 'Delete'
|
---|
115 | messages.comment_openid_signin = 'More about OpenID'
|
---|
116 | messages.comment_openid_help_title = 'Start using your OpenID'
|
---|
117 | messages.comment_openid_help_url = 'http://openid.net/get-an-openid/start-using-your-openid/'
|
---|
118 | messages.openid_fail_discover = 'Failed to discover OpenID service. Check your OpenID or "More about OpenID"?'
|
---|
119 | messages.flash_openid_expired = 'OpenID expired. Please login or authenticate OpenID again. Sorry for the inconvenient.'
|
---|
120 | messages.flash_openid_associated = 'OpenID associated'
|
---|
121 | messages.flash_associate_openid = 'Please login or register an account for this OpenID.'
|
---|
122 | messages.p_openid_not_registered = ("There is no Sahana account associated with that OpenID. "
|
---|
123 | + "Would you like to create one?")
|
---|
124 | messages.flash_openid_authenticated = 'OpenID authenticated successfully.'
|
---|
125 | messages.flash_openid_fail_authentication = 'OpenID authentication failed. (Error message: %s)'
|
---|
126 | messages.flash_openid_canceled = 'OpenID authentication canceled by user.'
|
---|
127 | messages.flash_openid_need_setup = 'OpenID authentication needs to be setup by the user with the provider first.'
|
---|
128 | messages.h_openid_login = 'OpenID Login'
|
---|
129 | messages.h_openid_list = 'OpenID List'
|
---|
130 | return messages
|
---|
131 |
|
---|
132 | def _define_alt_login_table(self):
|
---|
133 | """
|
---|
134 | Define the OpenID login table.
|
---|
135 | Note: type is what I used for our project. We're going to support 'fackbook' and
|
---|
136 | 'plurk' alternate login methods. Otherwise it's always 'openid' and you
|
---|
137 | may not need it. This should be easy to changed.
|
---|
138 | (Just remove the field of "type" and remove the
|
---|
139 | "and db.alt_logins.type == type_" in _find_matched_openid function)
|
---|
140 | """
|
---|
141 | db = self.db
|
---|
142 | table = db.define_table(
|
---|
143 | self.table_alt_logins_name,
|
---|
144 | Field('username', length=512, default=''),
|
---|
145 | Field('type', length=128, default='openid', readable=False),
|
---|
146 | Field('auth_user', self.table_user, readable=False),
|
---|
147 | )
|
---|
148 | table.username.requires = IS_NOT_IN_DB(db, table.username)
|
---|
149 | self.table_alt_logins = table
|
---|
150 |
|
---|
151 | def logout_url(self, next):
|
---|
152 | """
|
---|
153 | Delete the w2popenid record in session as logout
|
---|
154 | """
|
---|
155 | if self.environment.session.w2popenid:
|
---|
156 | del(self.environment.session.w2popenid)
|
---|
157 | return next
|
---|
158 |
|
---|
159 | def login_form(self):
|
---|
160 | """
|
---|
161 | Start to process the OpenID response if 'janrain_nonce' in request parameters
|
---|
162 | and not processed yet. Else return the OpenID form for login.
|
---|
163 | """
|
---|
164 | request = self.environment.request
|
---|
165 | if request.vars.has_key('janrain_nonce') and not self._processed():
|
---|
166 | self._process_response()
|
---|
167 | return self.auth()
|
---|
168 | return self._form()
|
---|
169 |
|
---|
170 | def get_user(self):
|
---|
171 | """
|
---|
172 | It supports the logout_url, implementing the get_user and login_form
|
---|
173 | for cas usage of gluon.tools.Auth.
|
---|
174 | """
|
---|
175 | environment = self.environment
|
---|
176 | request = environment.request
|
---|
177 | args = request.args
|
---|
178 |
|
---|
179 | if args[0] == 'logout':
|
---|
180 | return True # Let logout_url got called
|
---|
181 |
|
---|
182 | if environment.session.w2popenid:
|
---|
183 | w2popenid = environment.session.w2popenid
|
---|
184 | db = self.db
|
---|
185 | if (w2popenid.ok is True and w2popenid.oid): # OpenID authenticated
|
---|
186 | if self._w2popenid_expired(w2popenid): # OpenID expired
|
---|
187 | del(self.environment.session.w2popenid)
|
---|
188 | flash = self.messages.flash_openid_expired
|
---|
189 | environment.session.warning = flash
|
---|
190 | redirect(self.auth.settings.login_url)
|
---|
191 |
|
---|
192 | oid = self._remove_protocol(w2popenid.oid)
|
---|
193 | alt_login = self._find_matched_openid(db, oid)
|
---|
194 |
|
---|
195 | nextvar = self.nextvar
|
---|
196 | # This OpenID not in the database. If user logged in then add it
|
---|
197 | # into database, else ask user to login or register.
|
---|
198 | if not alt_login:
|
---|
199 | if self.auth.is_logged_in():
|
---|
200 | # TODO: ask first maybe
|
---|
201 | self._associate_user_openid(self.auth.user, oid)
|
---|
202 | if self.environment.session.w2popenid:
|
---|
203 | del(self.environment.session.w2popenid)
|
---|
204 | environment.session.flash = self.messages.flash_openid_associated
|
---|
205 | if request.vars.has_key(nextvar):
|
---|
206 | redirect(request.vars[nextvar])
|
---|
207 | redirect(self.auth.settings.login_next)
|
---|
208 |
|
---|
209 | if not request.vars.has_key(nextvar):
|
---|
210 | if ('first_time_login' not in w2popenid or
|
---|
211 | w2popenid.first_time_login == True):
|
---|
212 | page = 'register'
|
---|
213 | w2popenid.first_time_login = False
|
---|
214 | else:
|
---|
215 | page = 'login'
|
---|
216 | # no next var, add it and do login again
|
---|
217 | # so if user login or register can go back here to associate the OpenID
|
---|
218 | environment.session.flash = self.messages.p_openid_not_registered
|
---|
219 | redirect(self.environment.URL(r=request,
|
---|
220 | args=[page],
|
---|
221 | vars={nextvar:self.login_url}))
|
---|
222 | #self.login_form = self._form_with_notification()
|
---|
223 | if self.auth.settings.login_form in (self.auth, self):
|
---|
224 | self.auth.settings.login_form = self.auth
|
---|
225 | self.login_form = self.auth()
|
---|
226 | #environment.session.flash = self.messages.flash_associate_openid
|
---|
227 | return None # need to login or register to associate this openid
|
---|
228 |
|
---|
229 | # Get existed OpenID user
|
---|
230 | user = db(self.table_user.id==alt_login.auth_user).select().first()
|
---|
231 | if user:
|
---|
232 | if self.environment.session.w2popenid:
|
---|
233 | del(self.environment.session.w2popenid)
|
---|
234 | if 'username' in self.table_user.fields():
|
---|
235 | username = 'username'
|
---|
236 | elif 'email' in self.table_user.fields():
|
---|
237 | username = 'email'
|
---|
238 | return {username: user[username]} if user else None # login success (almost)
|
---|
239 |
|
---|
240 | return None # just start to login
|
---|
241 |
|
---|
242 | def _find_matched_openid(self, db, oid, type_='openid'):
|
---|
243 | """
|
---|
244 | Get the matched OpenID for given
|
---|
245 | """
|
---|
246 | query = ((db.alt_logins.username == oid) & (db.alt_logins.type == type_))
|
---|
247 | alt_login = db(query).select().first() # Get the OpenID record
|
---|
248 | return alt_login
|
---|
249 |
|
---|
250 | def _associate_user_openid(self, user, oid):
|
---|
251 | """
|
---|
252 | Associate the user logged in with given OpenID
|
---|
253 | """
|
---|
254 | print "[DB] %s authenticated" % oid
|
---|
255 | self.db.alt_logins.insert(username=oid, auth_user=user)
|
---|
256 |
|
---|
257 | def _form_with_notification(self):
|
---|
258 | """
|
---|
259 | Render the form for normal login with a notice of OpenID authenticated
|
---|
260 | """
|
---|
261 | form = DIV()
|
---|
262 | # TODO: check when will happen
|
---|
263 | if self.auth.settings.login_form in (self.auth, self):
|
---|
264 | self.auth.settings.login_form = self.auth
|
---|
265 | form = DIV(self.auth())
|
---|
266 |
|
---|
267 | register_note = DIV(P(B(self.messages.p_openid_not_registered)))
|
---|
268 | form.components.append(register_note)
|
---|
269 | return lambda: form
|
---|
270 |
|
---|
271 | def _remove_protocol(self, oid):
|
---|
272 | """
|
---|
273 | Remove https:// or http:// from oid url
|
---|
274 | """
|
---|
275 | protocol = 'https://'
|
---|
276 | if oid.startswith(protocol):
|
---|
277 | oid = oid[len(protocol):]
|
---|
278 | return oid
|
---|
279 | protocol = 'http://'
|
---|
280 | if oid.startswith(protocol):
|
---|
281 | oid = oid[len(protocol):]
|
---|
282 | return oid
|
---|
283 | return oid
|
---|
284 |
|
---|
285 | def _init_consumerhelper(self):
|
---|
286 | """
|
---|
287 | Initialize the ConsumerHelper
|
---|
288 | """
|
---|
289 | if not hasattr(self, "consumerhelper"):
|
---|
290 | self.consumerhelper = ConsumerHelper(self.environment.session,
|
---|
291 | self.db)
|
---|
292 | return self.consumerhelper
|
---|
293 |
|
---|
294 |
|
---|
295 | def _form(self, style=None):
|
---|
296 | w2popenid = self.environment.session.w2popenid
|
---|
297 | if (w2popenid and w2popenid.ok is True and w2popenid.oid): # OpenID authenticated
|
---|
298 | oid = self._remove_protocol(w2popenid.oid)
|
---|
299 | form = P('OpenID: ' + oid, ' ', A("Remove",
|
---|
300 | _href=self.environment.URL(r=self.environment.request,
|
---|
301 | args=['logout'],
|
---|
302 | vars={self.nextvar:self.login_url})
|
---|
303 | ))
|
---|
304 | return form
|
---|
305 |
|
---|
306 | form = DIV(H3(self.messages.h_openid_login), self._login_form(style))
|
---|
307 | return form
|
---|
308 |
|
---|
309 | def _login_form(self,
|
---|
310 | openid_field_label=None,
|
---|
311 | submit_button=None,
|
---|
312 | _next=None,
|
---|
313 | style=None):
|
---|
314 | """
|
---|
315 | Render the form for OpenID login
|
---|
316 | """
|
---|
317 | def warning_openid_fail(session):
|
---|
318 | session.warning = messages.openid_fail_discover
|
---|
319 |
|
---|
320 | style = style or """
|
---|
321 | background-attachment: scroll;
|
---|
322 | background-repeat: no-repeat;
|
---|
323 | background-image: url("http://wiki.openid.net/f/openid-16x16.gif");
|
---|
324 | background-position: 0% 50%;
|
---|
325 | background-color: transparent;
|
---|
326 | padding-left: 18px;
|
---|
327 | width: 400px;
|
---|
328 | """
|
---|
329 | style = style.replace("\n","")
|
---|
330 |
|
---|
331 | request = self.environment.request
|
---|
332 | session = self.environment.session
|
---|
333 | messages = self.messages
|
---|
334 | hidden_next_input = ""
|
---|
335 | if _next == 'profile':
|
---|
336 | profile_url = self.environment.URL(r=request, f='user', args=['profile'])
|
---|
337 | hidden_next_input = INPUT(_type="hidden", _name="_next", _value=profile_url)
|
---|
338 | form = FORM(openid_field_label or self.messages.label_alt_login_username,
|
---|
339 | INPUT(_type="input", _name="oid",
|
---|
340 | requires=IS_NOT_EMPTY(error_message=messages.openid_fail_discover),
|
---|
341 | _style=style),
|
---|
342 | hidden_next_input,
|
---|
343 | INPUT(_type="submit", _value=submit_button or messages.submit_button),
|
---|
344 | " ",
|
---|
345 | A(messages.comment_openid_signin,
|
---|
346 | _href=messages.comment_openid_help_url,
|
---|
347 | _title=messages.comment_openid_help_title,
|
---|
348 | _class='openid-identifier',
|
---|
349 | _target="_blank"),
|
---|
350 | _action=self.login_url
|
---|
351 | )
|
---|
352 | if form.accepts(request.vars, session):
|
---|
353 | oid = request.vars.oid
|
---|
354 | consumerhelper = self._init_consumerhelper()
|
---|
355 | url = self.login_url
|
---|
356 | return_to_url = self.return_to_url
|
---|
357 | if not oid:
|
---|
358 | warning_openid_fail(session)
|
---|
359 | redirect(url)
|
---|
360 | try:
|
---|
361 | if request.vars.has_key('_next'):
|
---|
362 | return_to_url = self.return_to_url + '?_next=' + request.vars._next
|
---|
363 | url = consumerhelper.begin(oid, self.realm, return_to_url)
|
---|
364 | except DiscoveryFailure:
|
---|
365 | warning_openid_fail(session)
|
---|
366 | redirect(url)
|
---|
367 | return form
|
---|
368 |
|
---|
369 | def _processed(self):
|
---|
370 | """
|
---|
371 | Check if w2popenid authentication is processed.
|
---|
372 | Return True if processed else False.
|
---|
373 | """
|
---|
374 | processed = (hasattr(self.environment.session, 'w2popenid') and
|
---|
375 | self.environment.session.w2popenid.ok is True)
|
---|
376 | return processed
|
---|
377 |
|
---|
378 | def _set_w2popenid_expiration(self, w2popenid):
|
---|
379 | """
|
---|
380 | Set expiration for OpenID authentication.
|
---|
381 | """
|
---|
382 | w2popenid.expiration = datetime.now() + timedelta(minutes=self.openid_expiration)
|
---|
383 |
|
---|
384 | def _w2popenid_expired(self, w2popenid):
|
---|
385 | """
|
---|
386 | Check if w2popenid authentication is expired.
|
---|
387 | Return True if expired else False.
|
---|
388 | """
|
---|
389 | return (not w2popenid.expiration) or (datetime.now() > w2popenid.expiration)
|
---|
390 |
|
---|
391 | def _process_response(self):
|
---|
392 | """
|
---|
393 | Process the OpenID by ConsumerHelper.
|
---|
394 | """
|
---|
395 | environment = self.environment
|
---|
396 | request = environment.request
|
---|
397 | request_vars = request.vars
|
---|
398 | consumerhelper = self._init_consumerhelper()
|
---|
399 | process_status = consumerhelper.process_response(request_vars, self.return_to_url)
|
---|
400 | if process_status == "success":
|
---|
401 | w2popenid = environment.session.w2popenid
|
---|
402 | user_data = self.consumerhelper.sreg()
|
---|
403 | environment.session.w2popenid.ok = True
|
---|
404 | self._set_w2popenid_expiration(w2popenid)
|
---|
405 | w2popenid.user_data = user_data
|
---|
406 | environment.session.flash = self.messages.flash_openid_authenticated
|
---|
407 | elif process_status == "failure":
|
---|
408 | flash = self.messages.flash_openid_fail_authentication % consumerhelper.error_message
|
---|
409 | environment.session.warning = flash
|
---|
410 | elif process_status == "cancel":
|
---|
411 | environment.session.warning = self.messages.flash_openid_canceled
|
---|
412 | elif process_status == "setup_needed":
|
---|
413 | environment.session.warning = self.messages.flash_openid_need_setup
|
---|
414 |
|
---|
415 | def list_user_openids(self):
|
---|
416 | messages = self.messages
|
---|
417 | environment = self.environment
|
---|
418 | request = environment.request
|
---|
419 | if request.vars.has_key('delete_openid'):
|
---|
420 | self.remove_openid(request.vars.delete_openid)
|
---|
421 |
|
---|
422 | query = self.db.alt_logins.auth_user == self.auth.user.id
|
---|
423 | alt_logins = self.db(query).select()
|
---|
424 | l = []
|
---|
425 | for alt_login in alt_logins:
|
---|
426 | username = alt_login.username
|
---|
427 | delete_href = environment.URL(r=request, f='user',
|
---|
428 | args=['profile'],
|
---|
429 | vars={'delete_openid': username})
|
---|
430 | delete_link = A(messages.a_delete, _href=delete_href)
|
---|
431 | l.append(LI(username, " ", delete_link))
|
---|
432 |
|
---|
433 | profile_url = environment.URL(r=request, f='user', args=['profile'])
|
---|
434 | #return_to_url = self.return_to_url + '?' + self.nextvar + '=' + profile_url
|
---|
435 | openid_list = DIV(H3(messages.h_openid_list), UL(l),
|
---|
436 | self._login_form(
|
---|
437 | _next='profile',
|
---|
438 | submit_button=messages.submit_button_add,
|
---|
439 | openid_field_label=messages.label_add_alt_login_username)
|
---|
440 | )
|
---|
441 | return openid_list
|
---|
442 |
|
---|
443 |
|
---|
444 | def remove_openid(self, openid):
|
---|
445 | query = self.db.alt_logins.username == openid
|
---|
446 | self.db(query).delete()
|
---|
447 |
|
---|
448 | class ConsumerHelper(object):
|
---|
449 | """
|
---|
450 | ConsumerHelper knows the python-openid and
|
---|
451 | """
|
---|
452 |
|
---|
453 | def __init__(self, session, db):
|
---|
454 | self.session = session
|
---|
455 | store = self._init_store(db)
|
---|
456 | self.consumer = openid.consumer.consumer.Consumer(session, store)
|
---|
457 |
|
---|
458 | def _init_store(self, db):
|
---|
459 | """
|
---|
460 | Initialize Web2pyStore
|
---|
461 | """
|
---|
462 | if not hasattr(self, "store"):
|
---|
463 | store = Web2pyStore(db)
|
---|
464 | session = self.session
|
---|
465 | if not session.has_key('w2popenid'):
|
---|
466 | session.w2popenid = Storage()
|
---|
467 | self.store = store
|
---|
468 | return self.store
|
---|
469 |
|
---|
470 | def begin(self, oid, realm, return_to_url):
|
---|
471 | """
|
---|
472 | Begin the OpenID authentication
|
---|
473 | """
|
---|
474 | w2popenid = self.session.w2popenid
|
---|
475 | w2popenid.oid = oid
|
---|
476 | auth_req = self.consumer.begin(oid)
|
---|
477 | auth_req.addExtension(SRegRequest(required=['email','nickname']))
|
---|
478 | url = auth_req.redirectURL(return_to=return_to_url, realm=realm)
|
---|
479 | return url
|
---|
480 |
|
---|
481 | def process_response(self, request_vars, return_to_url):
|
---|
482 | """
|
---|
483 | Complete the process and
|
---|
484 | """
|
---|
485 | resp = self.consumer.complete(request_vars, return_to_url)
|
---|
486 | if resp:
|
---|
487 | if resp.status == openid.consumer.consumer.SUCCESS:
|
---|
488 | self.resp = resp
|
---|
489 | if hasattr(resp, "identity_url"):
|
---|
490 | self.session.w2popenid.oid = resp.identity_url
|
---|
491 | return "success"
|
---|
492 | if resp.status == openid.consumer.consumer.FAILURE:
|
---|
493 | self.error_message = resp.message
|
---|
494 | return "failure"
|
---|
495 | if resp.status == openid.consumer.consumer.CANCEL:
|
---|
496 | return "cancel"
|
---|
497 | if resp.status == openid.consumer.consumer.SETUP_NEEDED:
|
---|
498 | return "setup_needed"
|
---|
499 | return "no resp"
|
---|
500 |
|
---|
501 | def sreg(self):
|
---|
502 | """
|
---|
503 | Try to get OpenID Simple Registation
|
---|
504 | http://openid.net/specs/openid-simple-registration-extension-1_0.html
|
---|
505 | """
|
---|
506 | if self.resp:
|
---|
507 | resp = self.resp
|
---|
508 | sreg_resp = SRegResponse.fromSuccessResponse(resp)
|
---|
509 | return sreg_resp.data if sreg_resp else None
|
---|
510 | else:
|
---|
511 | return None
|
---|
512 |
|
---|
513 |
|
---|
514 | class Web2pyStore(OpenIDStore):
|
---|
515 | """
|
---|
516 | Web2pyStore
|
---|
517 |
|
---|
518 | This class implements the OpenIDStore interface. OpenID stores take care
|
---|
519 | of persisting nonces and associations. The Janrain Python OpenID library
|
---|
520 | comes with implementations for file and memory storage. Web2pyStore uses
|
---|
521 | the web2py db abstration layer. See the source code docs of OpenIDStore
|
---|
522 | for a comprehensive description of this interface.
|
---|
523 | """
|
---|
524 |
|
---|
525 | def __init__(self, database):
|
---|
526 | self.database = database
|
---|
527 | self.table_oid_associations_name = 'oid_associations'
|
---|
528 | self.table_oid_nonces_name = 'oid_nonces'
|
---|
529 | self._initDB()
|
---|
530 |
|
---|
531 | def _initDB(self):
|
---|
532 |
|
---|
533 | if self.table_oid_associations_name not in self.database:
|
---|
534 | self.database.define_table(self.table_oid_associations_name,
|
---|
535 | SQLField('server_url', 'string', length=2047, required=True),
|
---|
536 | SQLField('handle', 'string', length=255, required=True),
|
---|
537 | SQLField('secret', 'blob', required=True),
|
---|
538 | SQLField('issued', 'integer', required=True),
|
---|
539 | SQLField('lifetime', 'integer', required=True),
|
---|
540 | SQLField('assoc_type', 'string', length=64, required=True)
|
---|
541 | )
|
---|
542 | if self.table_oid_nonces_name not in self.database:
|
---|
543 | self.database.define_table(self.table_oid_nonces_name,
|
---|
544 | SQLField('server_url', 'string', length=2047, required=True),
|
---|
545 | SQLField('time_stamp', 'integer', required=True),
|
---|
546 | SQLField('salt', 'string', length=40, required=True)
|
---|
547 | )
|
---|
548 |
|
---|
549 | def storeAssociation(self, server_url, association):
|
---|
550 | """
|
---|
551 | Store associations. If there already is one with the same
|
---|
552 | server_url and handle in the table replace it.
|
---|
553 | """
|
---|
554 |
|
---|
555 | db = self.database
|
---|
556 | query = (db.oid_associations.server_url == server_url) & (db.oid_associations.handle == association.handle)
|
---|
557 | db(query).delete()
|
---|
558 | db.oid_associations.insert(server_url = server_url,
|
---|
559 | handle = association.handle,
|
---|
560 | secret = association.secret,
|
---|
561 | issued = association.issued,
|
---|
562 | lifetime = association.lifetime,
|
---|
563 | assoc_type = association.assoc_type), 'insert '*10
|
---|
564 |
|
---|
565 | def getAssociation(self, server_url, handle=None):
|
---|
566 | """
|
---|
567 | Return the association for server_url and handle. If handle is
|
---|
568 | not None return the latests associations for that server_url.
|
---|
569 | Return None if no association can be found.
|
---|
570 | """
|
---|
571 |
|
---|
572 | db = self.database
|
---|
573 | query = (db.oid_associations.server_url == server_url)
|
---|
574 | if handle:
|
---|
575 | query &= (db.oid_associations.handle == handle)
|
---|
576 | rows = db(query).select(orderby=db.oid_associations.issued)
|
---|
577 | keep_assoc, _ = self._removeExpiredAssocations(rows)
|
---|
578 | if len(keep_assoc) == 0:
|
---|
579 | return None
|
---|
580 | else:
|
---|
581 | assoc = keep_assoc.pop() # pop the last one as it should be the latest one
|
---|
582 | return Association(assoc['handle'],
|
---|
583 | assoc['secret'],
|
---|
584 | assoc['issued'],
|
---|
585 | assoc['lifetime'],
|
---|
586 | assoc['assoc_type'])
|
---|
587 |
|
---|
588 | def removeAssociation(self, server_url, handle):
|
---|
589 | db = self.database
|
---|
590 | query = (db.oid_associations.server_url == server_url) & (db.oid_associations.handle == handle)
|
---|
591 | return db(query).delete() != None
|
---|
592 |
|
---|
593 | def useNonce(self, server_url, time_stamp, salt):
|
---|
594 | """
|
---|
595 | This method returns Falase if a nonce has been used before or its
|
---|
596 | time_stamp is not current.
|
---|
597 | """
|
---|
598 |
|
---|
599 | db = self.database
|
---|
600 | if abs(time_stamp - time.time()) > nonce.SKEW:
|
---|
601 | return False
|
---|
602 | query = (db.oid_nonces.server_url == server_url) & (db.oid_nonces.time_stamp == time_stamp) & (db.oid_nonces.salt == salt)
|
---|
603 | if db(query).count() > 0:
|
---|
604 | return False
|
---|
605 | else:
|
---|
606 | db.oid_nonces.insert(server_url = server_url,
|
---|
607 | time_stamp = time_stamp,
|
---|
608 | salt = salt)
|
---|
609 | return True
|
---|
610 |
|
---|
611 | def _removeExpiredAssocations(self, rows):
|
---|
612 | """
|
---|
613 | This helper function is not part of the interface. Given a list of
|
---|
614 | association rows it checks which associations have expired and
|
---|
615 | deletes them from the db. It returns a tuple of the form
|
---|
616 | ([valid_assoc], no_of_expired_assoc_deleted).
|
---|
617 | """
|
---|
618 |
|
---|
619 | db = self.database
|
---|
620 | keep_assoc = []
|
---|
621 | remove_assoc = []
|
---|
622 | t1970 = time.time()
|
---|
623 | for r in rows:
|
---|
624 | if r['issued'] + r['lifetime'] < t1970:
|
---|
625 | remove_assoc.append(r)
|
---|
626 | else:
|
---|
627 | keep_assoc.append(r)
|
---|
628 | for r in remove_assoc:
|
---|
629 | del db.oid_associations[r['id']]
|
---|
630 | return (keep_assoc, len(remove_assoc)) # return tuple (list of valid associations, number of deleted associations)
|
---|
631 |
|
---|
632 | def cleanupNonces(self):
|
---|
633 | """
|
---|
634 | Remove expired nonce entries from DB and return the number
|
---|
635 | of entries deleted.
|
---|
636 | """
|
---|
637 |
|
---|
638 | db = self.database
|
---|
639 | query = (db.oid_nonces.time_stamp < time.time() - nonce.SKEW)
|
---|
640 | return db(query).delete()
|
---|
641 |
|
---|
642 | def cleanupAssociations(self):
|
---|
643 | """
|
---|
644 | Remove expired associations from db and return the number
|
---|
645 | of entries deleted.
|
---|
646 | """
|
---|
647 |
|
---|
648 | db = self.database
|
---|
649 | query = (db.oid_associations.id > 0)
|
---|
650 | return self._removeExpiredAssocations(db(query).select())[1] #return number of assoc removed
|
---|
651 |
|
---|
652 | def cleanup(self):
|
---|
653 | """
|
---|
654 | This method should be run periodically to free the db from
|
---|
655 | expired nonce and association entries.
|
---|
656 | """
|
---|
657 |
|
---|
658 | return self.cleanupNonces(), self.cleanupAssociations()
|
---|
659 |
|
---|