From 73b14e1df497999ad8b554367a4ab64f9f50aaf8 Mon Sep 17 00:00:00 2001
From: Kevin Champion <kevin.champion@gmail.com>
Date: Thu, 19 Apr 2012 19:58:16 -0400
Subject: [PATCH] Issue #1540268 by kevinchampion: Fix hook_init bug, add
 logout feature, improve code standards, update
 documentation.

---
 INSTALL.txt    |   51 ----------
 README.txt     |  109 +++++++++++++++++++++
 cosign.info    |    2 +-
 cosign.install |   10 ++
 cosign.module  |  289 +++++++++++++++++++++++++++++++++++++++----------------
 5 files changed, 325 insertions(+), 136 deletions(-)
 delete mode 100644 INSTALL.txt
 create mode 100644 README.txt
 create mode 100644 cosign.install

diff --git a/INSTALL.txt b/INSTALL.txt
deleted file mode 100644
index 2ffc01e..0000000
--- a/INSTALL.txt
+++ /dev/null
@@ -1,51 +0,0 @@
-
-http://drupal.org/project/cosign
-
-Installation instructions
----------------------------
-* you must be running your server within a cosign infrastructure.
-	More info at: http://www.weblogin.org/
-* download the cosign module
-* download the webserver_auth module, upon which cosign depends
-* copy the modules into your drupal modules directory
-* enable the modules through the ?q=admin/build/modules page
-* add the logout block to your page with the ?q=admin/build/block page
-
-Warning 
---------
-When enabling cosign, all users mentioned in the user table will be
-copied into the authmap table. This means that any pre-existing local
-users will be converted over to user@$_SERVER[REMOTE_REALM].  This may
-cause a mis-identification, and potential security issue if you had
-a user registered locally, and that name is mapped to someone else
-when using cosign.
-
-For example, I may have chosen the username 'willie' for my local
-drupal user. Although when using cosign, my username is 'willn'. In
-this case, by turning on cosign I've lost access to my former username,
-and another user would have access to all of my previous documents,
-comments, preferences, etc.  A more serious situation would be if your
-admin user doesn't share your username.  If you turn on cosign without
-changing your admin username, then you suddenly lose your
-administration account.
-
-Be sure to vet your authmap list before "opening the doors" to
-your userbase. It may be wise to only enable cosign authentication
-on a brand new drupal installation.
-
-You will become the admin user:
----------------------------------
-When you turn on the cosign module, if your local drupal
-username is not the same as your cosign username (taken from
-$_SERVER['REMOTE_USER']), then a destructive change will happen,
-and your cosign user will become the new admin, and assume ownership
-of the previous admin's postings, etc. This is to prevent someone
-else from administering your drupal instance. Since you have the
-permission to enable this module, then you must be authorized to be
-the administrator.
-
-Turning off the cosign module does not revert the admin user back to
-using a local drupal account.
-
----------------------------------
-Written by Willie Northway
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..36ed557
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,109 @@
+
+http://drupal.org/project/cosign
+
+Installation instructions
+---------------------------
+* you must be running your server within a cosign infrastructure.
+	More info at: http://www.weblogin.org/
+* download the cosign module
+* read this README file, paying close attention to the Warning at bottom
+* copy the modules into your drupal module directory
+* enable the module through the admin/modules page
+
+Htaccess
+---------
+If your Drupal site is installed in the web root of the server, you'll likely
+need to add a line to your .htaccess file for the root directory of the Drupal
+site.
+
+If you have Drupal installed at the DocumentRoot level (/) and are using Clean
+URLs, add the line
+
+  RewriteCond %{REQUEST_URI} !=/cosign/valid
+
+to the Clean URL rewrite rule to prevent the web browser from looping. Cosign
+usese this path, and thus you need to add an exception so that Drupal does not
+think this path is a Drupal path. If you are seeing the symptom whereby you're
+running into an infinite redirect problem when logging in, this is likely the
+cure.
+
+For a stock Drupal 7 site's .htaccess file that has not been modified, your new
+rule will look like this:
+
+  # Pass all requests not referring directly to files in the filesystem to
+  # index.php. Clean URLs are handled in drupal_environment_initialize().
+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteCond %{REQUEST_FILENAME} !-d
+  RewriteCond %{REQUEST_URI} !=/favicon.ico
+  RewriteCond %{REQUEST_URI} !=/cosign/valid
+  RewriteRule ^ index.php [L]
+
+
+Configuration
+--------------
+The configuration page can be found at: /admin/config/system/cosign
+
+Pay particular attention to the settings as they dictate key behavior regarding
+how Cosign module will behave. Of particular importance are the following
+settings:
+
+* Logout Path: path to the Cosign logout script to use. Note that when using the
+  local logout script (default), the user's service cookie will be destroyed
+  immediately before being redirect to central Cosgin for logout, whereas when
+  using the central Cosign server logout path the user's service cookie will
+  remain active for up to a minute.
+
+  The reason for this has to do with the architecture of Cosign. On every HTTP
+  request, the local Cosign server checks the user's service cookie to see if
+  it's more than 60 seconds old. If it is, it checks with the central Cosign
+  server to see if the user is still logged in to Cosign. If the user is, the
+  local Cosign server generates a new service cookie for the user.
+
+  If the user is logged out of the central Cosign server, it may take the local
+  server up to 60 seconds before it will check back in with the central Cosign
+  server, and find out that the user is no longer authenticated.
+
+* Logout users from Drupal when their Cosign session expires: Under certain
+  circumstances it is possible for the user to logout of Cosign through a
+  different Cosign protected application and still be able to access Drupal
+  permissions protected pages because Cosign module does not automatically
+  log the user out of Drupal when it no longer detects a remote_user from the
+  service cookie. Whether or not this is possible depends on how your local
+  Cosign server is configured and how Drupal is configured.
+
+* Redirect for users without a Drupal account: A path or full url of the page
+  to redirect to if the user authenticates through Cosign successfully but does
+  not have a corresponding Drupal account. This setting is only relevant if you
+  have selected 'No' for either the 'Auto-create Users' setting or the 'Allow
+  friend accounts' setting. If either is set to 'No', it becomes possible for
+  the user to be unable to login to Drupal. Along with this path for the
+  redirect, both of these settings have their own message setting that allows
+  you to customize the explanatory message displayed to users when they run into
+  one of these conditions.
+
+Warning
+--------
+The Drupal 7.x-1.x version of Cosign module does not include the method of
+copying existing users to the authmap table. This means that this module should
+be used with great care as Drupal usernames will be matched with Cosign login
+names automatically when they login. Therefore, if a local Drupal username
+matches a Cosign username, but the two usernames are for two different people,
+the Cosign username holder will affectively take over the Drupal user account.
+
+For example, I may have chosen the username 'willie' for my local
+drupal user. Although when using cosign, my username is 'willn'. In
+this case, by turning on cosign I've lost access to my former username,
+and another user would have access to all of my previous documents,
+comments, preferences, etc.  A more serious situation would be if your
+admin user doesn't share your username.  If you turn on cosign without
+changing your admin username, then you suddenly lose your
+administration account.
+
+Be sure to vet your user list before "opening the doors" to
+your userbase. It may be wise to only enable cosign authentication
+on a brand new drupal installation. Pay particular attention to the username of
+user 1. It should match the Cosign username of the person who should have full
+administrative access to the site.
+
+---------------------------------
+Written by Willie Northway and Kevin Champion
diff --git a/cosign.info b/cosign.info
index 3f9cbff..15152ee 100644
--- a/cosign.info
+++ b/cosign.info
@@ -1,5 +1,5 @@
 name = Cosign
-description = Manages automatic user login and supplies cosign logout bar
+description = Manages automatic user login and logout
 core = 7.x
 
 
diff --git a/cosign.install b/cosign.install
new file mode 100644
index 0000000..efcb23f
--- /dev/null
+++ b/cosign.install
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * Implements hook_install().
+ */
+function cosign_install() {
+  drupal_set_message(st("Your Cosign module settings are available under !link",
+    array( '!link' => l(st('Administer > Configuration > System > Cosign'),  'admin/config/system/cosign'))
+  ));
+}
diff --git a/cosign.module b/cosign.module
index 5392e45..f74c80e 100644
--- a/cosign.module
+++ b/cosign.module
@@ -3,10 +3,38 @@
 /**
  * The Cosign Module
  *
- * This module manages automatic user login and supplies cosign logout bar.
+ * This module manages automatic user login and logout.
  */
 
-function cosign_init() {
+/**
+ * Implements hook_menu_get_item_alter().
+ *
+ * We can't use hook_init here in Drupal 7 because some changes were made to
+ * the order that hooks run. Essentially, there a bunch of things that run in
+ * between hook_boot and hook_init in D7. One of these is menu_get_item, which
+ * runs the menu access callback functions on the request and returns
+ * $router_item['access'] before hook_init even runs. This means that if a user
+ * links directly to an https page with any drupal permissions protection, the
+ * user will hit an access denied page immediately after authenticating through
+ * cosign. Refreshing that page or navigating to a new one does not produce a
+ * second access denied because by that point $user is defined from hook_init on
+ * the previous page request.
+ *
+ * @see http://drupal.org/node/928160
+ * @see http://drupal.org/node/553944
+ */
+function cosign_menu_get_item_alter(&$router_item, $path, $original_map) {
+  // When retrieving the router item for the current path...
+  if ($path == $_GET['q']) {
+    // Call a function that handles user login.
+    cosign_route();
+  }
+}
+
+/**
+ * Helper function, does heavy lifting of cosign login and logout.
+ */
+function cosign_route() {
   global $user;
   if ($_GET['q'] == 'user/logout') {
     cosign_logout();
@@ -16,33 +44,45 @@ function cosign_init() {
   if (drupal_get_path_alias($_GET['q']) == 'invalidlogin') {
     return TRUE;
   }
-  
+
   $cosign_name = '';
 
   // Make sure we get the remote user whichever way it is available.
   if (isset($_SERVER['REDIRECT_REMOTE_USER'])) {
     $cosign_name = $_SERVER['REDIRECT_REMOTE_USER'];
-  } elseif (isset($_SERVER['REMOTE_USER'])) {
+  }
+  elseif (isset($_SERVER['REMOTE_USER'])) {
     $cosign_name = $_SERVER['REMOTE_USER'];
   }
 
   // If friend accounts are not allowed, log them out
   if (variable_get('cosign_allow_friend_accounts', 0) == 0 && stristr($cosign_name, '@')) {
-    $default_invalid_logout = 'user/logout';
-    $invalid_login = variable_get('cosign_invalid_login', $default_invalid_logout);
-    drupal_goto($invalid_login);
-  }
-  
-  if ($user->uid) {
-    // Do nothing: user already logged in Drupal with session data matching
-    return TRUE;
+    watchdog('cosign', 'User attempted login using a university friend account and the friend account configuration setting is turned off: @remote_user', array('@remote_user' => $cosign_name), WATCHDOG_NOTICE);
+
+    drupal_set_message(variable_get('cosign_friend_account_message', t('We were not able to log you in because this site does not accept university friend accounts.')), 'status');
+
+    drupal_goto(variable_get('cosign_invalid_login', 'access-denied'));
   }
 
-  // Perform some cleanup so plaintext passwords aren't available 
+  // Perform some cleanup so plaintext passwords aren't available
   unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
 
   if (!empty($cosign_name)) {
-    
+
+    if ($user->uid) {
+
+      $account = user_load_by_name($cosign_name);
+
+      if (!empty($account->uid) && $user->uid == $account->uid) {
+        // Do nothing: user already logged in Drupal with session data matching
+        return TRUE;
+      }
+      else {
+        // log user out and proceed
+        cosign_drupal_logout();
+      }
+    }
+
     // Retrieve user credentials
       $result = db_select('users', 'u')
                   ->fields('u', array('uid', 'name'))
@@ -50,53 +90,71 @@ function cosign_init() {
                   ->execute();
       $user_info = $result->fetchObject();
 
-           if (empty($user_info)) {
-      
-      if (variable_get('cosign_autocreate', 0) == 1) {
-        $default_email_domain = 'umich.edu';
-        
-        $new_user->name = $cosign_name;
+      if (empty($user_info)) {
+        if (variable_get('cosign_autocreate', 0) == 1) {
+
+          $default_email_domain = 'umich.edu';
+          $new_user->name = $cosign_name;
+
+          if (stristr($new_user->name, '@')) {
+            // friend account
+            $new_user->mail = $cosign_name;
+          }
+          else {
+            $new_user->mail = $cosign_name . '@' . variable_get('cosignautocreate_email_domain', $default_email_domain);
+          }
 
-        if (stristr($new_user->name, '@')) {
-          // friend account
-          $new_user->mail = $cosign_name;
-        } else {
-          $new_user->mail = $cosign_name . '@' . variable_get('cosignautocreate_email_domain', $default_email_domain);
+          user_external_login_register($new_user->name, 'cosignautocreate');
+
+          $edit = array(
+            'mail' => $new_user->mail,
+          );
+
+          user_save($user, $edit);
+
+          drupal_session_regenerate();
         }
-        
-        user_external_login_register($new_user->name, 'cosignautocreate');
-                
-        $edit = array(
-          'mail' => $new_user->mail,
-        );
-        
-        user_save($user, $edit);
+        else {
+          watchdog('cosign', 'User attempted login but does not have a Drupal account and the auto-create user configuration setting is turned off: @remote_user', array('@remote_user' => $cosign_name), WATCHDOG_NOTICE);
 
-        drupal_session_regenerate();
-      } else {
-        $default_invalid_logout = 'user/logout';
-        $invalid_login = variable_get('cosign_invalid_login', $default_invalid_logout);
-        drupal_goto($invalid_login);
+          drupal_set_message(variable_get('cosign_invalid_login_message', t('We were not able to log you in because you do not have an account on this site yet.')), 'status');
+
+          drupal_goto(variable_get('cosign_invalid_login', 'access-denied'));
+        }
       }
-       } else {
-           $user = user_load_by_name($cosign_name);
-            drupal_session_regenerate();
+      else {
+        // Log the user in.
+        $account = user_load_by_name($cosign_name);
+
+        $form_state['uid'] = $account->uid;
+
+        user_login_submit(array(), $form_state);
       }
   }
+  else {
+    // remote_user is empty, if user is still logged in, log user out
+    if ($user->uid && variable_get('cosign_autologout', 1)) {
+      cosign_drupal_logout();
+    }
+  }
   return TRUE;
 }
 
+/**
+ * Form constructor for the cosign administrative configuration form.
+ *
+ * @ingroup forms
+ */
 function cosign_admin() {
-  $logout_machine = 'https://weblogin.umich.edu/cgi-bin/logout'; 
-  $script_path = '/cgi-bin/logout';
-  $logout_path = $logout_machine . $script_path;
+
+  $default_logout_path = 'https://' . $_SERVER['SERVER_NAME'] . '/cgi-bin/logout';
   $form['cosign_logout_path'] = array(
     '#type' => 'textfield',
     '#title' => t('Logout Path'),
-    '#default_value' => variable_get('cosign_logout_path', $logout_path),
+    '#default_value' => variable_get('cosign_logout_path', $default_logout_path),
     '#size' => 80,
     '#maxlength' => 200,
-    '#description' => t("The address (including http(s)) of the machine and script path for logging out."),
+    '#description' => t("The address (including http(s)) of the machine and script path for logging out. Cosign has two options for logout. The default of the Cosign module is to use the local server logout script. This script immediately kills the local service cookie and redirects the user to central Cosign weblogin to logout. Alternatively, you can instead change the logout address to the central Cosign weblogin logout script (for the University of Michigan, it is: https://weblogin.umich.edu/cgi-bin/logout). By redirecting the user directly to the central Cosign weblogin logout script, the user's service cookie will not be immediately destroyed. Instead, it will remain active for a period of up to 1 minute during which time the user's HTTP requests will remain authenticated by the local service cookie."),
   );
 
   $logout_to =  'http://' . $_SERVER['SERVER_NAME'] . base_path();
@@ -109,66 +167,93 @@ function cosign_admin() {
     '#description' => t("The address to redirect users to after they have logged out."),
   );
 
-  $invalid_login = 'http://' . $_SERVER['SERVER_NAME'] . base_path() . "invalidlogin";
-  $form['cosign_invalid_login'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Page displayed for unAuthorized Cosign User'),
-    '#default_value' => variable_get('cosign_invalid_login', $invalid_login),
-    '#size' => 80,
-    '#maxlength' => 200,
-    '#description' => t("The address of the server and page name displayed for unauthorized Cosign user. <b>***NOTE*** you MUST create this page!</b>"),
-  );
-
   $default_email_domain = 'umich.edu';
- 
+
   $YesNo = array(
     1 => 'Yes',
     0 => 'No',
   );
-    
+
+  $form['cosign_autologout'] = array(
+    '#type' => 'select',
+    '#title' => t('Logout users from Drupal when their Cosign session expires?'),
+    '#description' => t('If not selected, when users logout of Cosign, they will not also be automatically logged out of Drupal. This can lead to quite a bit of confusion depending on how you have Cosign configured on the server and how your site is setup. For instance, a user could logout of Cosign through a different Cosign protected application, return to the site, and access protected pages after thinking they\'ve already logged out. This can be a security issue if users think they\'re logged out of Drupal but are not.'),
+    '#options' => $YesNo,
+    '#default_value' => variable_get('cosign_autologout', 1),
+  );
+
   $form['cosign_autocreate'] = array(
     '#type' => 'select',
     '#title' => 'Auto-create Users?',
     '#options' => $YesNo,
-    '#default_value' => variable_get('cosign_autocreate', 0)
+    '#default_value' => variable_get('cosign_autocreate', 0),
   );
 
-  $form['cosign_autocreate_email_domain'] = array(
+  $form['cosign_invalid_login'] = array(
     '#type' => 'textfield',
-    '#title' => t('Logout Path'),
+    '#title' => t('Redirect for users without a Drupal account'),
+    '#default_value' => variable_get('cosign_invalid_login', 'access-denied'),
+    '#size' => 80,
+    '#maxlength' => 200,
+    '#description' => t("The address or path where users should land if they authenticate through Cosign, but do not have a corresponding Drupal account, and thus are not logged in to Drupal. This is only relevant if you have 'Auto-create Users' set to 'No' or 'Allow friend accounts' set to 'No'. If you set this to an internal path that doesn't exist, you must create the page you set it to."),
+  );
+
+  $form['cosign_invalid_login_message'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Message displayed to users after they authenticate to Cosign but don\'t have a Drupal account'),
+    '#default_value' => variable_get('cosign_invalid_login_message', t('We were not able to log you in because you do not have an account on this site yet.')),
+    '#description' => t("This message is only relevant if you have 'Auto-create Users' set to 'No'. When a user logs in who doesn't have an account, this message will tell them what happened."),
+  );
+
+  // This setting is not used anywhere anymore. May be useful for some feature
+  // that was removed and may be built back in in the future.
+  /*$form['cosign_autocreate_email_domain'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Email domain'),
     '#default_value' => variable_get('cosign_autocreate_email_domain', $default_email_domain),
     '#size' => 80,
     '#maxlength' => 200,
     '#description' => t("The default email domain to use"),
-  );
-  
+  );*/
+
   $form['cosign_allow_friend_accounts'] = array(
     '#type' => 'select',
     '#title' => 'Allow friend accounts?',
     '#options' => $YesNo,
-    '#default_value' => variable_get('cosign_allow_friend_accounts', 0)
+    '#default_value' => variable_get('cosign_allow_friend_accounts', 0),
+  );
+  $form['cosign_friend_account_message'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Message displayed to users after they authenticate to Cosign with a friend account'),
+    '#default_value' => variable_get('cosign_friend_account_message', t('We were not able to log you in because this site does not accept university friend accounts.')),
+    '#description' => t("This message is only relevant if you have 'Allow friend accounts' set to 'No'. When a friend account user logs in, this message will tell them that they don't have access."),
   );
 
   return system_settings_form($form);
 }
 
-
+/**
+ * Implements hook_menu().
+ */
 function cosign_menu() {
   $items['admin/config/system/cosign'] = array(
   // $items['admin/cosign'] = array(
-    'title' => 'Cosign_Auth',
-    'description' => 'Control the Cosign_auth module behavior',
+    'title' => 'Cosign',
+    'description' => 'Control the Cosign module behavior',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('cosign_admin'),
     'access arguments' => array('access administration pages'),
     'type' => MENU_NORMAL_ITEM,
   );
-  
+
   return $items;
 }
 
+/**
+ * Implements hook_form_alter().
+ */
 function cosign_form_alter(&$form, &$form_state, $form_id) {
- 
+
   if ($form_id == "user_login_block") {
     $form['#action'] = 'user/login';
     unset($form['name']);
@@ -177,9 +262,9 @@ function cosign_form_alter(&$form, &$form_state, $form_id) {
     $form['links']['#value'] = '<a href="https://' . $_SERVER['HTTP_HOST'] . request_uri() . '">Login</a>';
   }
 
-   if ($form_id == "user_login") {
-     drupal_goto('https://' . $_SERVER['SERVER_NAME'] . base_path());
-   }
+  if ($form_id == "user_login") {
+    drupal_goto('https://' . $_SERVER['SERVER_NAME'] . base_path());
+  }
 
   if ($form_id == "user_profile_form") {
     if (isset($form['account']['name'])) {
@@ -189,6 +274,9 @@ function cosign_form_alter(&$form, &$form_state, $form_id) {
   }
 }
 
+/**
+ * Implements hook_help().
+ */
 function cosign_help($path, $arg) {
   switch ($path) {
     case 'admin/modules#description':
@@ -199,19 +287,22 @@ function cosign_help($path, $arg) {
   }
 }
 
+/**
+ * Helper function, handles drupal and cosign logout.
+ *
+ * @see user_logout()
+ */
 function cosign_logout() {
+  // The following method for logging the user out of Drupal comes from
+  // user_logout in core.
   global $user;
 
-  // Destroy the current session:
-  if (isset($user)) {
-    session_destroy();
-    $_SESSION = array();
-  }
+  watchdog('user', 'Session closed for %name.', array('%name' => $user->name));
 
-  module_invoke_all('user', 'user/logout', NULL, $user);
+  module_invoke_all('user_logout', $user);
 
-  // Load the anonymous user
-  $user = drupal_anonymous_user();
+  // Destroy the current session, and reset $user to the anonymous user.
+  session_destroy();
 
   $default_logout_path = 'https://' . $_SERVER['SERVER_NAME'] . '/cgi-bin/logout';
   $default_logout_to =   'http://' . $_SERVER['SERVER_NAME'] . base_path();
@@ -224,14 +315,41 @@ function cosign_logout() {
   return TRUE;
 }
 
-function cosign_block_info() {
+/**
+ * Helper function, logs user out of drupal with no redirect.
+ *
+ * Alternative to user_logout(), cosign_drupal_logout() doesn't redirect.
+ *
+ * @see user_logout()
+ */
+function cosign_drupal_logout() {
+  // The following method for logging the user out of Drupal comes from
+  // user_logout in core.
+  global $user;
+
+  watchdog('user', 'Session closed for %name.', array('%name' => $user->name));
+
+  module_invoke_all('user_logout', $user);
+
+  // Destroy the current session, and reset $user to the anonymous user.
+  session_destroy();
+}
+
+// Not sure what the purpose is of creating a block anymore.
+/**
+ * Implements hook_block_info().
+ */
+/*function cosign_block_info() {
   global $user;
   $blocks['cosign'] = array(
       'info' => t('Cosign status and logout'),
-  ); 
-  return $blocks; 
-}
+  );
+  return $blocks;
+}*/
 
+/**
+ * Implements hook_disable().
+ */
 function cosign_disable() {
   $module = 'cosign';
 
@@ -245,6 +363,9 @@ function cosign_disable() {
   drupal_set_message(t('Cosign module has been disabled'));
 }
 
+/**
+ * Implements hook_enable().
+ */
 function cosign_enable() {
   $errmsg = '';
   $realm = '';
-- 
1.7.7

