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
-~----------~----~----~----~------~----~------~--~---

Reply via email to