plugins/yubikey/auth.php
author Dan
Thu, 26 Feb 2009 01:19:45 -0500
changeset 2 6edc6ebb3b39
parent 0 9d2c4f04a0d0
child 3 d0fe7acaf0e8
permissions -rw-r--r--
Minor: if input OTP is empty and password looks like OTP, now copies password to OTP in memory and treats password field as OTP. Will require patch in Enano trunk to work for html login.

<?php

// hook into auth
$plugins->attachHook('login_process_userdata_json', 'return yubikey_auth_hook_json($userinfo, $req["level"], @$req["remember"]);');
// hook into special page init
$plugins->attachHook('session_started', 'yubikey_add_special_pages();');

function yubikey_auth_hook_json(&$userdata, $level, $remember)
{
  global $db, $session, $paths, $template, $plugins; // Common objects
  global $lang;
  
  $do_validate_otp = false;
  $do_validate_user = false;
  $do_validate_pass = false;
  
  $user_flag = ( $level >= USER_LEVEL_CHPREF ) ? YK_SEC_ELEV_USERNAME : YK_SEC_NORMAL_USERNAME;
  $pass_flag = ( $level >= USER_LEVEL_CHPREF ) ? YK_SEC_ELEV_PASSWORD : YK_SEC_NORMAL_PASSWORD;
  
  $auth_log_prefix = ( $level >= USER_LEVEL_CHPREF ) ? 'admin_' : '';
  
  // Sort of a hack: if the password looks like an OTP and the OTP field is empty, use the password as the OTP
  if ( empty($userdata['yubikey_otp']) && preg_match('/^[cbdefghijklnrtuv]{44}$/', $userdata['password'] ) )
  {
    $userdata['yubikey_otp'] = $userdata['password'];
  }
  
  if ( !empty($userdata['username']) )
  {
    // get flags
    $q = $db->sql_query('SELECT user_id, user_yubikey_flags FROM ' . table_prefix . "users WHERE " . ENANO_SQLFUNC_LOWERCASE . "(username) = '" . $db->escape(strtolower($userdata['username'])) . "';");
    if ( !$q )
      $db->die_json();
    
    if ( $db->numrows() < 1 )
    {
      // Username not found - let the main login function handle it
      $db->free_result();
      return null;
    }
    list($user_id, $flags) = $db->fetchrow_num();
    $flags = intval($flags);
    // At the point the username is validated.
    $do_validate_user = false;
    $do_validate_pass = $flags & $pass_flag;
    if ( empty($userdata['yubikey_otp']) )
    {
      // no OTP was provided
      // make sure the user has allowed logging in with no OTP
      if ( !($flags & YK_SEC_ALLOW_NO_OTP) )
      {
        // We also might have no Yubikeys enrolled.
        $q = $db->sql_query('SELECT 1 FROM ' . table_prefix . "yubikey WHERE user_id = $user_id;");
        if ( !$q )
          $db->die_json();
        
        if ( $db->numrows() > 0 )
        {
          // Yep at least one key is enrolled.
          // I don't think these should be logged because they'll usually just be innocent mistakes.
          $db->free_result();
          return array(
              'mode' => 'error',
              'error' => 'yubiauth_err_must_have_otp'
            );
        }
        // Nope, no keys enrolled, user hasn't enabled Yubikey support
        $db->free_result();
      }
      // we're ok, use normal password auth
      return null;
    }
    else
    {
      // user did enter an OTP
      $do_validate_otp = true;
    }
  }
  else if ( !empty($userdata['yubikey_otp']) )
  {
    // we have an OTP, but no username to work with
    $yubi_uid = substr($userdata['yubikey_otp'], 0, 12);
    if ( !preg_match('/^[cbdefghijklnrtuv]{12}$/', $yubi_uid ) )
    {
      return array(
          'mode' => 'error',
          'error' => 'yubiauth_err_invalid_otp'
        );
    }
    $q = $db->sql_query('SELECT u.user_id, u.username, u.user_yubikey_flags FROM ' . table_prefix . "users AS u\n"
                      . "  LEFT JOIN " . table_prefix . "yubikey AS y\n"
                      . "    ON ( y.user_id = u.user_id )\n"
                      . "  WHERE y.yubi_uid = '$yubi_uid'\n"
                      . "  GROUP BY u.user_yubikey_flags;");
    if ( !$q )
      $db->_die();
    
    if ( $db->numrows() < 1 )
    {
      if ( !$do_validate_pass )
        $session->sql('INSERT INTO ' . table_prefix . "logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES\n"
                   . '  (\'security\', \'' . $auth_log_prefix . 'auth_bad\', '.time().', \''.enano_date('d M Y h:i a').'\', \'(Yubikey)\', '
                      . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')');
      
      return array(
          'mode' => 'error',
          'error' => 'yubiauth_err_key_not_authorized'
        );
    }
    
    list($user_id, $username, $flags) = $db->fetchrow_num();
    $do_validate_otp = true;
    $do_validate_user = $flags & $user_flag;
    $do_validate_pass = $flags & $pass_flag;
  }
  else
  {
    // Nothing - no username or OTP. This request can't be used; throw it out.
    return array(
        'mode' => 'error',
        'error' => 'yubiauth_err_nothing_provided'
      );
  }
  if ( $do_validate_otp )
  {
    // We need to validate the OTP.
    $otp_check = yubikey_validate_otp($userdata['yubikey_otp']);
    if ( !$otp_check['success'] )
    {
      if ( !$do_validate_pass )
        $session->sql('INSERT INTO ' . table_prefix . "logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES\n"
                   . '  (\'security\', \'' . $auth_log_prefix . 'auth_bad\', '.time().', \''.enano_date('d M Y h:i a').'\', \'(Yubikey)\', '
                      . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')');
      return array(
          'mode' => 'error',
          'error' => 'yubiauth_err_' . $otp_check['error']
        );
    }
  }
  if ( $do_validate_user )
  {
    if ( empty($username) )
    {
      return array(
          'mode' => 'error',
          'error' => 'yubiauth_err_must_have_username'
        );
    }
    if ( strtolower($username) !== strtolower($userdata['username']) )
    {
      // Username incorrect
      if ( !$do_validate_pass )
        $session->sql('INSERT INTO ' . table_prefix . "logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES\n"
                   . '  (\'security\', \'' . $auth_log_prefix . 'auth_bad\', '.time().', \''.enano_date('d M Y h:i a').'\', \'(Yubikey)\', '
                      . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')');
      return array(
          'mode' => 'error',
          'error' => 'invalid_credentials'
        );
    }
  }
  // Do we need to have the password validated?
  if ( $do_validate_pass )
  {
    // Yes; return and let the login API continue
    return null;
  }
  else
  {
    // No password required; validated, issue session key
    $session->sql('INSERT INTO ' . table_prefix . "logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES\n"
                   . '  (\'security\', \'' . $auth_log_prefix . 'auth_good\', '.time().', \''.enano_date('d M Y h:i a').'\', \'' . $db->escape($userdata['username']) . '\', '
                      . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')');
        
    $q = $db->sql_query('SELECT password FROM ' . table_prefix . "users WHERE user_id = $user_id;");
    if ( !$q )
      $db->_die();
    
    list($password) = $db->fetchrow_num();
    $db->free_result();
    
    $session->register_session($user_id, $userdata['username'], $password, $level, $remember);
    return true;
  }
}

function yubikey_add_special_pages()
{
  global $db, $session, $paths, $template, $plugins; // Common objects
  global $lang;
  
  $paths->add_page(array(
      'name' => $lang->get('yubiauth_specialpage_yubikey'),
      'urlname' => 'Yubikey',
      'namespace' => 'Special',
      'visible' => 0, 'protected' => 0, 'comments_on' => 0, 'special' => 0
    ));
}

function page_Special_Yubikey()
{
  global $db, $session, $paths, $template, $plugins; // Common objects
  
  header('Content-type: text/javascript');
  /*
  if ( isset($_GET['validate_otp']) )
  {
    echo enano_json_encode(yubikey_validate_otp($_GET['validate_otp']));
    return true;
  }
  */
  if ( isset($_GET['get_flags']) || isset($_POST['get_flags']) )
  {
    $yubi_uid = substr($_REQUEST['get_flags'], 0, 12);
    if ( !preg_match('/^[cbdefghijklnrtuv]{12}$/', $yubi_uid) )
    {
      return print enano_json_encode(array(
          'mode' => 'error',
          'error' => 'invalid_otp'
        ));
    }
    $q = $db->sql_query('SELECT u.user_yubikey_flags FROM ' . table_prefix . "users AS u\n"
                      . "  LEFT JOIN " . table_prefix . "yubikey AS y\n"
                      . "    ON ( y.user_id = u.user_id )\n"
                      . "  WHERE y.yubi_uid = '$yubi_uid'\n"
                      . "  GROUP BY u.user_yubikey_flags;");
    if ( !$q )
      $db->_die();
    
    if ( $db->numrows() < 1 )
    {
      return print enano_json_encode(array(
          'mode' => 'error',
          'error' => 'key_not_authorized'
        ));
    }
    
    list($flags) = $db->fetchrow_num();
    
    echo enano_json_encode(array(
        // We strip YK_SEC_ALLOW_NO_OTP here for security reasons.
        'flags' => intval($flags & ~YK_SEC_ALLOW_NO_OTP)
      ));
    
    return true;
  }
}