Hi,

I'm working on implementing a two factor authentication module for drupal. Basically, there's a third field on any user login form that asks for a character from a grid (the grid is randomly generated for each user on registration). The form asks for say the character(token) in cell A4 on the grid, and checks it.

Currently I've got the validation of the token in a validation function associated with the token field. But even if that validation fails, the user module runs it's stuff and authenticates the user OK. So that while it displays a form with errors, it has still actually logged you in (if you go to another page, the nav links'll show).

Is this because I'm validating in my own function? Do I need to override the entire form validation?
I tried that but I can't get the hook to execute on submission (overriding the drupal_login_validate hook with a tokenAuthentication_login_validate hook).

Thanks in advance for your help, and please let me know if there's anything I can clarify.
Cheers,
Toby

Comments

moshe weitzman’s picture

you should be able to use form element validatuion as you describe. but if that doesn't work, try hook_user('validate')

half_brick’s picture

Yea, I still can't seem to get it to work. Because even if my additional validation fails, the user module validation still works and validates in the background.

Using hook_user doesn't seem like an option as only the user and password are passed to that, and I'm validating against an additional form element that I've added.

ilo’s picture

your validation returns FALSE the original validation has nothing to do then.. something should be wrong in your module flow, there's no way a validation returns false and this result is ignored.

If you prefer you can show some of the code to see what can be wrong.. (the interesting parts will be: the hooks form_alter, and validate, the submit doesn't care for this purpose).

half_brick’s picture

Hi ilo,
My code follows. The first function is the validation for the token field, and the second is the form_alter for the login form. I have tried implementing the hook to validate the whole login form, but I could not get it to "catch". What function signature would I need to implement the whole form validation hook?

Thanks for your help,
Toby

/* called to validate the token field */
function token_validate($form){
	
	$name = $form['#post']['name'];
	
	$token = $form['#post']['token'];
	
	// get uid
	$result = db_query("SELECT uid FROM {users} WHERE name = '$name'");
	$row = mysql_fetch_array($result);
	$uid = $row['uid'];

	if(!is_numeric($uid)) form_set_error('token', 'Invalid token');
	
	// get the expected token
	$gridResult = db_query("SELECT grid FROM {tokenauthentication_grids} WHERE uid = $uid");
	if(mysql_num_rows($gridResult) == 0){ // user does not have grid, so don't authenticate against it
	 	return null;
	}
	$gridRow = mysql_fetch_array($gridResult);
	
	$grid = $gridRow['grid'];
	

	// get expected value
	$token_grid_x = $form['#post']['token_grid_x'];
	$token_grid_y = $form['#post']['token_grid_y'];
	
	// get numeric y value
	$chars = 'abcdefghijkmnopqrstuvwxyz';
	$token_grid_y = strpos($chars, $token_grid_y);

	// get corresponding from grid
	$gridSize = variable_get('tokenAuth_gridSize', 10);
	if($gridSize > 26) $gridSize = 26;
	
	$num_below = $token_grid_y * $gridSize;
	$total_before = $token_grid_x + $num_below;
	
	
	$expected = substr($grid, $total_before, 1);
	
	if($expected != $token) {
		//drupal_set_message("Invalid token");
		//user_logout();
		form_set_error('token', 'Invalid token');
		return FALSE;
	}
		
}




/* Alter the login form to allow users to provide their token */
function tokenauthentication_form_alter($form_id, &$form){
	if($form_id == 'user_login_block'){
		// generate a grid coordinate that they must use
		$gridSize = variable_get('tokenAuth_gridSize', 10);
		if($gridSize > 26) $gridSize = 26;
		$x = rand(0, $gridSize);
		$y = rand(0, $gridSize);
		
		// get the letter for y
		$chars = 'abcdefghijkmnopqrstuvwxyz';
		$yLetter = substr($chars, $y, 1);
		
		$form["token_grid_x"] = array('#type' => 'hidden', '#value' => $x);
		$form["token_grid_y"] = array('#type' => 'hidden', '#value' => $yLetter);						 
		
		$form["token"] = array("#type" => "textfield",
							 "#title"=>  "Token",
							 "#description" => 'Please enter the letter from the cell <b>' . $yLetter . $y . '</b> in your grid. If you do not have a grid, leave it blank.',
							 "#maxlength" =>  60,
							 "#size" => 15,
							 "#required" =>  false,
							 "#weight" => -5,
							 '#validate' => array('token_validate' => array()));
		// push the other two fields above the token
		$form["name"]["#weight"] = -7;
		$form["pass"]["#weight"] = -6;
		
	}
	
}
ilo’s picture

For the hook to work should be named as the module name. I'm not sure, but if the login block shows your challenge, then I guess the module name es tokenauthentication, and in that scenary the validation function should be renamed to :

function tokenauthentication_validate($form){
..
}

Try this and see and lets see what happends then :D

The previous information is wrong. I mixed somethings in the same post in a really bad solution.

The right solution for your problem should be to attach your own validation function to the form in the _form_alter hook this way:

<?php
/* Alter the login form to allow users to provide their token */
function tokenauthentication_form_alter($form_id, &$form){
    if(($form_id == 'user_login_block') || ($form_id == 'user_login')){
        // generate a grid coordinate that they must use
        ...
       
        ...
        $form['#validate']['token_validate'] = array();
    }
   
}

And you should set the appropiate parameters to the token_validate function:

/* called to validate the token field */
function token_validate($form_id, $form_values){   

    $name = $form['#post']['name'];   
    $token = $form['#post']['token'];   
    ...       
}

You can read the form submitted values using the apropiate $form_values array instead of the '#post' array.

When you talk about main login form, is this the user login form instead of the login block? to get that form_id you can view the html source of the page where the form appears and look for the hidden input type with name "form_id" (drupal doesn't hide it). In the login form the form_id is :

<input type="hidden" name="form_id" id="edit-user-login" value="user_login"  />

ok?

half_brick’s picture

Hi ilo,

That's pretty much what I had initially. With the form setting the token_validate function as the validation function for the token field.
That works in that it returns an error on an incorrect token, but it seems the login still takes place, because on a refresh of the page the user is logged in.

The issue is more that when the token_validate function decides the form is invalid, the form error is set, but the user.module still logs in.

Do you have any more ideas? I would have thought that any hook that invalidates in the form validation should prevent the rest of the validate functions from performing any actions....?

Cheers,
Toby

ilo’s picture

and it's true.. looks like an issue in the Forms API, and even if you return FALSE or if you execute form_set_error the form doesn't break and the login occurs. I've been looking at captchas code and other modules and seems that the only way working for me is to execute a drupal_goto inside of the form flow.

  if ($error){
     form_set_error('field','uh.. error and blah');
     drupal_goto(get_destination());
  }

This one was the only test working for me, and I tried many variations of the #validation property in the form, as a field validation, a form validation.. and so. Maybe we need to do some little more tests and report as an issue to the drupal core if we can't get any point, as this is not a solution. I wonder the status of a form if you run a drupal_goto inside a validation hook.

ilo’s picture

Problem:

--

Attaching validations to the login form will not disrupt the login operation even if the validators set errors in the form fields..

--

sadly, it's an ugly trick in core user module, so fix should be done carefully to avoid breaking any other modules workflow..

Here are my findings on the issue:

Here is the validation hook for the login form:

function user_login_validate($form_id, $form_values) {
  if ($form_values['name']) {
    if (user_is_blocked($form_values['name'])) {
      // blocked in user administration
      form_set_error('login', t('The username %name has not been activated or is blocked.', array('%name' => $form_values['name'])));
    }
    else if (drupal_is_denied('user', $form_values['name'])) {
      // denied by access controls
      form_set_error('login', t('The name %name is a reserved username.', array('%name' => $form_values['name'])));
    }
    else if ($form_values['pass']) {
      $user = user_authenticate($form_values['name'], trim($form_values['pass']));

      if (!$user->uid) {
        form_set_error('login', t('Sorry, unrecognized username or password. <a href="@password">Have you forgotten your password?</a>', array('@password' => url('user/password'))));
        watchdog('user', t('Login attempt failed for %user.', array('%user' => $form_values['name'])));
      }
    }
  }
}

The user_authenticate actually loads the user if the auth is right..
And here is the submit hook for the login form:

function user_login_submit($form_id, $form_values) {
  global $user;
  if ($user->uid) {
    // To handle the edge case where this function is called during a
    // bootstrap, check for the existence of t().
    if (function_exists('t')) {
      $message = t('Session opened for %name.', array('%name' => $user->name));
    }
    else {
       $message = "Session opened for ". check_plain($user->name);
    }
    watchdog('user', $message);

    // Update the user table timestamp noting user has logged in.
    db_query("UPDATE {users} SET login = %d WHERE uid = %d", time(), $user->uid);

    user_module_invoke('login', $form_values, $user);

    sess_regenerate();
    return 'user/'. $user->uid;
  }
}

My reading about this submit hook, it's, somewhere in the flow the code is already loaded previous to reach the submit hook. As you can read the code, the user is not loaded in the submit hook, but it's comparing if the current session has a user ID to perform the login finishing operations, as log the entry and update last_login timestamp..

This is the reason why any other external validation will work with the login for, as the submit hook is not used to actually login and load the user, but just to register the operation..

so.. where the user is loaded then?

Take a look at the authentication function:

function user_authenticate($name, $pass) {
  global $user;

  // Try to log in the user locally. Don't set $user unless successful.
  if ($account = user_load(array('name' => $name, 'pass' => $pass, 'status' => 1))) {
    $user = $account;
    return $user;
  }
  ...

Currently, what user_authenticate does is not only to check if the user and password for the non blocked user are right, but it also loads the user in the current session:


Definition
---------

user_authenticate($name, $pass)
modules/user/user.module, line 1218

Description
----------
Try to log in the user locally.

Return value
------------

A $user object, if successful.

So it's the login validation actually loading the user, before the other hooked validations return any error or even if there where any errors attached to the form.

It's an important flow error, I guess, but also it's an important change that may affect other modules..

Solutions...

1º break the form operation using drupal_goto in your validation function so the form will not continue executing validation hooks.. there's no way to ensure the login validation has not yet been executed.

2º modify one of the fields (name or pass) and overwritte user object if uid != 0 with guest data, so it will not login, or it's session will be deleted.

3º patch the user.module to load the user in the current session only in the submit operation, instead of the validation.

I think we can't go further here, but to report this as an issue for the core module. Hope this can help you!!

half_brick’s picture

Cheers for your help ilo. Glad to know it's not just my ignorance :)
At least now I can implement some kind of hack, without fearing there's a better way.

UPDATE: If anyone wants to know, I just threw a call to session_destroy(); before I return false from the validation function. This destroys the session that the user module prematurely created. This would probably fail if the user module validation executed after my module's validation, but I've set the weight for my module as lighter than user. This seems to work; good enough for now, it's only a uni project.

Cheers,
Toby

ilo’s picture

In my case setup, where my module is validated before the user module (due to it's name), the hook validate at user.module is never executed, so the drupal_goto works fine for now, waiting to see what happends with authentication tasks last patches.

By the way, for the tests about your problem I started with a simple module with the needed hooks, and after putting some code inside to test your problem I finally released a login module (http://www.drupal.org/project/login_security) :D ..

I'm going to read about the session flow, the other idea I had in mind is to reset the global $user variable as a guest user, but I did none of them.

Note: Is there a way I could get your module to check it out? I'm nervous to see other's implementations!

half_brick’s picture

reformatt’s picture

OMG, this is the best post since sliced bread!!
It helped me sooo much, I couldnt figure out why cron couldnt node_save() for different users :)