Yannick Gingras pisze:
> Greetings Pyloneers,
>
> It's be a long time since I hacked something with Pylons but things
> are moving favorably now. After a flash demo of our great tools, I
> was able to convince out lead architect that Pylons was the right
> choice for our next web app.
Hello!
I'm new to this group, never posted anything here before, but I think
I've got something just right for you. I have done implementation of
HTTP-Auth Basic (Digest is no problem also, but you'll have to change
the controller a bit) and also web based user authentication.
So. Here is my solution for http-auth-basic. Mind you it's not very
fancy, because we are not using http-auth anymore. Also we are using
Elixir as our database model system on top of SQLAlchemy. Since this is
obsolete piece of code it may be not usable, but you can take something
from this and do your own http-auth login controller. It's not
guaranteed it will work.
--- cut ---
from YOURPROJECTNAME.lib.base import *
import base64
class LoginController(BaseController):
""""""
def index(self):
""""""
c.error = session.get('error')
return render('/login/login.mako')
def login(self):
""""""
if not request.headers.has_key('Authorization'):
response.status_code = 401
response.headers['WWW-Authenticate'] = "Basic realm=\"REALM\""
return "Not authenticated"
auth = request.headers['Authorization']
auth_list = auth.split(' ')
auth_type = auth_list[0]
if auth_type == 'Basic':
auth_login, auth_pass =
base64.b64decode(auth_list[1]).split(':')
elif auth_type == 'Digest':
return 'Digest authentication is not yet implemented'
# FIXME: Get account object from database
account_obj = model.Account.query().get_by(email = auth_login)
if account_obj is None:
# FIXME: Show error
abort(404)
# Check if account is active
if account_obj.status == 0:
# FIXME:
session['error'] = 'Account is not active! Cannot login.'
session.save()
redirect_to(action = 'index')
# If passwords do not match
if account_obj.password != auth_pass:
session['error'] = 'Error: Passwords do not match!'
session.save()
redirect_to(action = 'index')
# User authentication succeeded, save logged_user_id in session
session['logged_user_id'] = account_obj.id
session.save()
return redirect_to('login/loggedin.mako')
--- cut ---
As for authenticating website users through website form we have devised
our own system based on Challenge-Response Protocol. Basically the idea
is to create a login controller like this:
controllers/login.py
--- cut ---
class LoginController(BaseController): #TODO: docstring
"""
Login controller class
"""
hash_func = config.get('passwd_hash_func')
def index(self):
"""
Shows login form
"""
c.error = session['error'] if session.has_key('error') else None
# Generate hashed random seed
rand_str = str(datetime.now()) + str(random.randint(100,1000000))
rseed = self.hash_func(rand_str).hexdigest()
# Save this random seed into session
session['login_rseed'] = rseed
session.save()
# Setup form
c.form = login_form(action = h.url_for(controller = 'login',
action = 'login'),
attrs = {'onsubmit' : 'javascript:return
validate_login_form();'},
value = {'rseed' : rseed}
)
c.heading = u"Logowanie"
return render('/login/login.mako')
@validate(form = login_form, error_handler = 'index')
def login(self): #TODO: docstring
"""
Performs login checks
"""
# Check if user is logged
if session.get('logged_user'):
session['error'] = 'User already logged in!'
session.save()
return render('/login/loggedin.mako')
# Get values passed from login form
email = str(request.POST.get('email'))
password = str(request.POST.get('password'))
secure = str(request.POST.get('secure'))
# Remove previous errors
if session.get('error'):
del(session['error'])
session.save()
# Get account from database
account_obj = model.Account.query().get_by(email = email)
if account_obj is None:
session['error'] = 'Error: User does not exist!'
session.save()
redirect_to(action = 'index')
# Generate hash for comparison
apasswd = account_obj.password
# Check if password was sent in secure manner
if secure == 'yes':
# Check if random seed has been passed in session
rseed = session.get('login_rseed')
if not rseed:
session['error'] = 'Error: Random seed doesn\'t exist!'
session.save()
redirect_to(action = 'index')
# Generate hash from seed and password
seedpass = self.hash_func(rseed + apasswd).hexdigest()
else:
# Password was cleartext so we have to hash it
seedpass = apasswd
password = self.hash_func(password).hexdigest()
# Check Challenge Response hashes
if seedpass != password:
session['error'] = 'Error: Bad email or password!'
session.save()
redirect_to(action = 'index')
# If hashes match save user as logged in
session['logged_user'] = email
session['logged_user_id'] = account_obj.id
session.save()
# Send user back to the page he originally wanted to get to
if session.get('path_before_login'):
# Get path before login
redir_url = session['path_before_login']
# Remove path before login from session
del(session['path_before_login'])
session.save()
# Redirect to url
redirect_to(redir_url)
else:
return render('/login/loggedin.mako')
def logout(self): #TODO: docstring
"""
Performs user logout
"""
# Check if user is logged
if not session.get('logged_user'):
session['error'] = 'User is not logged in!'
session.save()
return redirect_to(action = 'index')
del(session['logged_user'])
del(session['logged_user_id'])
session.save()
return render('login/logout.mako')
--- cut ---
And here is the login template:
templates/login/login.mako
--- cut ---
# -*- coding: utf-8 -*-
<%inherit file="/main.mako" />
% if c.error:
<div id="errorbox">
${ c.error }
</div>
% endif
<noscript>
<div id="errorbox">
<h2>Warning!</h2>
You don't have JavaScript enabled. Your password will be unprotected! If
you want to login securely enable Javascript in your browser settings.
</div>
</noscript>
${c.form}
<%def name="metatags()">
${ h.javascript_include_tag('prototypeUtils', 'md5', 'helpers', builtins
= True) }
</%def>
--- cut ---
We are also using ToscaWidgets for creating forms so we have something
like this for the login form:
widgets/forms/login/login.py
--- cut ---
from toscawidgets.widgets import forms
from toscawidgets.api import WidgetsList
from toscawidgets.widgets.forms.validators import Email, UnicodeString,
NotEmpty
forms.FormField.engine_name = "mako"
class LoginForm(forms.Form):
""""""
template = "mako:mibu.templates.forms.default"
submit_text = u"Login"
class fields(WidgetsList):
email = forms.TextField(
validator = Email(not_empty = True),
label_text = u"E-mail:"
)
password = forms.PasswordField(
validator = UnicodeString(not_empty = True),
label_text = u"Password:"
)
rseed = forms.HiddenField(default = '')
secure = forms.HiddenField(default = '')
login_form = LoginForm("login_form")
--- cut ---
The last thing to do is to correct BaseController. We are specifying
requires_auth variable that you can further use in your own controllers
so unauthenticated users are automatically redirected to the login form
and after successful login they are brought back to the page they
requested.
requires_auth is either a list of controller methods or boolean value.
If it's bool and True every action in the controller requires
authentication.
lib/base.py
--- cut ---
class BaseController(WSGIController):
requires_auth = []
def __before__(self, action):
# Check if controller needs authorization
if type(self.requires_auth) == types.BooleanType:
if self.requires_auth:
if 'logged_user' not in session:
session['path_before_login'] = request.path_info
session.save()
return redirect_to(h.url_for(controller = 'login'))
# If requires authorization is list
else:
# Check if action needs authorization
if action in self.requires_auth:
if 'logged_user' not in session:
session['path_before_login'] = request.path_info
session.save()
return redirect_to(h.url_for(controller = 'login'))
def __call__(self, environ, start_response):
"""Invoke the Controller"""
# WSGIController.__call__ dispatches to the Controller method
# the request is routed to. This routing information is
# available in environ['pylons.routes_dict']
return WSGIController.__call__(self, environ, start_response)
--- cut ---
The final thing to setup is JavaScript that encrypts the password before
submiting login form.
public/javascripts/helpers.js
--- cut ---
// String trim function
String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g,
''); }
// Login form validation
function validate_login_form() {
var email = $('login_form_email').getValue();
var password = $('login_form_password').getValue();
var rseed = $('login_form_rseed').getValue();
var secure = $('login_form_secure').getValue();
if (email.trim() == '')
{
alert('Email is not set!');
return false;
}
// FIXME: email address validation
if (password.trim() == '')
{
alert('Password is not set!');
return false;
}
if (rseed.trim() == '')
{
alert('Login error occured!');
return false;
}
// Generate hash
var challenge = gen_challenge(password, rseed);
if (challenge == false)
{
alert('Login error occured!');
return false;
}
// Hide form and change password to challenge hash
hide_element('login_form');
Form.Element.setValue('password', challenge);
Form.Element.setValue('secure', 'yes');
$('login_form').submit();
return true;
}
// Challenge-response authentication generator
function gen_challenge(password, rseed)
{
if (password.trim() == '')
return false;
if (rseed.trim() == '')
return false;
return hex_md5(rseed + hex_md5(password));
}
function show_element(id_name)
{
$(id_name).style.display = 'block';
}
function hide_element(id_name)
{
$(id_name).style.display = 'none';
}
--- cut ---
And this is it. All you have to do to use this authentication is simply
specifying requires_auth = True in your controller or as list of
function names in controller that requires authentication.
If javascript is enabled for user the password will be encrypted for
challenge-response authentication (you can change your hashing function
to another one instead of md5 so you will be secured against md5
dictionary attacks). If javascript is disabled, user still can login,
but he will get a warning that the password will be sent in an unsecure
manner. Of course you can create a decorator so login attempts would be
redirected to https site, but I don't really understand how decorators
work, so I won't help.
I hope this helps. Maybe some of you could refine this code?
Best regards,
Karol Tomala
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"pylons-discuss" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at
http://groups.google.com/group/pylons-discuss?hl=en
-~----------~----~----~----~------~----~------~--~---