<?php
// $Id: googleauth.module,v 1.1 2007/07/13 18:22:22 ssnider Exp $

/**
 * @file
 * Implement a single sign on provider for Google Apps for Education
 */

/**
 * Implementation of hook_help().
 */
function googleauth_help($section) {
  switch ($section) {
    case 'admin/help#googleauth':
      $output = '<p>'.t('The googleauth module implements a provider for the Google Apps Single Sign On API. All users with the <em>use googleauth</em> permission will be able to authenticate with your Google Apps install').'</p>';
      return $output;
      break;
   }
}

/**
* Implementation of hook_menu().
*/
function googleauth_menu()
{
  $items = array();
 
  $items['googleauth/signin'] = array(
    'title' => 'Provider',
    'page callback' => 'googleauth_sign_in',
    'access arguments' => array('use googleauth'),
    'type' => MENU_CALLBACK
  );
  $items['googleauth/signout'] = array(
    'title' => 'Google Sign Out',
    'page callback' => 'googleauth_sign_out',
    'access arguments' => array('use googleauth'),
    'type' => MENU_CALLBACK
  );
  $items['admin/settings/googleauth'] = array(
    'title' => 'googleauth module settings',
    'description' => 'googleauth configuration settings for file paths',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('googleauth_adminpage'),
    'access arguments' => array('administer googleauth'),
    'type' => MENU_NORMAL_ITEM,
  );
return $items;
}

/**
 * Reads in an XML string that has been gzipped and then base64 encoded, and returns the plaintext.
 */

function decode_and_unzip($req) {
  $req = base64_decode($req);
  $req1 = gzinflate($req);
  //Note: One of my servers doesn't have this function for some reason, so use an alternative
  if ($req1 === FALSE) {
    $req1 = gzuncompress($req);
  }
  return $req1;
}


/**
 * Gets only the parts of the SAMLRequest that we actually want.
 */
function get_the_interesting_parts($req) {
  $parser = xml_parser_create();
  $result1 = xml_parse_into_struct($parser, $req, $result, $index);
  if ($result1 == 0) {
    return FALSE;
  }
  $interesting['acs_url'] = $result[0]['attributes']['ASSERTIONCONSUMERSERVICEURL'];
  $interesting['issue_instant'] = $result[0]['attributes']['ISSUEINSTANT'];
  $interesting['provider_name'] = $result[0]['attributes']['PROVIDERNAME'];
  $interesting['request_ID'] = $result[0]['attributes']['ID'];
  return $interesting;
}

/**
 * Get the wierd time format that SAML requires
 */

function get_wierd_time($time_off) {
  return gmdate('Y-m-d\TH:i:s\Z', time() + $time_off);
}

/**
 * Generate a SAML ID
 */

function get_random_id() {
  $random_pool = 'abcdefghijklmnopqrstuvwxyz';
  $the_id = '';
  
  for ($i = 0; $i < 40; $i++) {
    $the_id .= $random_pool[rand(0, strlen($random_pool)-1)];
  }
  
  return $the_id;

}

/**
 * Generate the response xml document, and sign it with the key
 */

function get_the_actual_response($i, $public, $private) {
  $curr = <<<EOF
<samlp:Response ID="{{RESPONSE_ID}}" IssueInstant="{{ISSUE_INSTANT}}" Version="2.0" Destination="{{DESTINATION}}" InResponseTo="{{REQUEST_ID}}" xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" /><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#{{RSADSA}}-sha1" /><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" /><DigestValue></DigestValue></Reference></SignedInfo><SignatureValue></SignatureValue><KeyInfo><KeyValue></KeyValue></KeyInfo></Signature><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><Assertion ID="{{ASSERTION_ID}}" IssueInstant="{{ISSUE_INSTANT}}" Version="2.0"	xmlns="urn:oasis:names:tc:SAML:2.0:assertion"><Issuer>{{ISSUER_DOMAIN}}</Issuer><Subject><NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress">{{USERNAME_STRING}}</NameID><SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"/></Subject><Conditions NotBefore="{{NOT_BEFORE}}" NotOnOrAfter="{{NOT_ON_OR_AFTER}}"></Conditions><AuthnStatement AuthnInstant="{{AUTHN_INSTANT}}"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement></Assertion></samlp:Response>
EOF;
  $curr = str_replace('{{USERNAME_STRING}}', $i['user_name'], $curr); 
  $curr = str_replace('{{RESPONSE_ID}}', get_random_id(), $curr);
  $curr = str_replace('{{ISSUE_INSTANT}}', get_wierd_time(0), $curr);
  $curr = str_replace('{{AUTHN_INSTANT}}', get_wierd_time(0), $curr);
  $curr = str_replace('{{NOT_BEFORE}}', $i['not_before'], $curr);
  $curr = str_replace('{{NOT_ON_OR_AFTER}}', $i['not_on_or_after'], $curr);
  $curr = str_replace('{{ASSERTION_ID}}', get_random_id(), $curr);
  $curr = str_replace('{{RSADSA}}', strtolower($i['key_type']), $curr);
  $curr = str_replace('{{REQUEST_ID}}', $i['request_ID'], $curr);
  $curr = str_replace('{{DESTINATION}}', $i['acs_url'], $curr);
  $curr = str_replace('{{ISSUER_DOMAIN}}', $i['domain_name'], $curr);
  $temp = tempnam('/var/tmp', 'TO_SIGN_');
  if (!$h = fopen($temp, 'w')) {
    drupal_set_message('Cannot open temporary file!');
    return FALSE;
  }
  if (fwrite($h, $curr) === FALSE) {
    drupal_set_message('Cannot write to temporary file');
    return FALSE;
  }
  fclose($h);
  $temp_out = tempnam('/var/tmp', 'SIGNED_');
  exec('chmod a+r ' . $temp);
  exec('chmod a+r ' . $temp_out, $trash);
  $result = exec(variable_get('googleauth_path_to_xmlsec', '/usr/bin/xmlsec1') . ' sign --privkey-pem ' . $private . ' --pubkey-der ' . $public . ' --output ' . $temp_out . ' ' . $temp, $result);
  unlink($temp);
  $actual_response = file_get_contents($temp_out);
  if (!$actual_response) {
    drupal_set_message('Signing failed!');
    return FALSE;
  }
  unlink($temp_out);
  return $actual_response;
}

/**
 * Retrieves the username from drupal
 */

function get_drupal_username() {
  global $user;
  return $user->name;
}

/**
 * Menu callback which responds to the Google Auth attempt.
 */
function googleauth_sign_in() {
  $user_name = get_drupal_username();
  $SAMLRequest = $_GET['SAMLRequest']; // This is base64 encoded and gzipped.
  if (!$SAMLRequest) {
    //NOTE: This happens if you haven't logged in before attempting to
    // authenticate to google. For some reason, the login page doesn't
    // preserve GET variables, and according to the comments I've read,
    // there doesn't seem to be a way around it without modifying Drupal
    // Core itself (messy!).
    drupal_set_message(t("SAMLRequest not present! Please try again!"), 'error');
    drupal_goto();
  }
  $SAMLRequest = decode_and_unzip($SAMLRequest);
  if ($SAMLRequest === FALSE) {
    drupal_set_message(t("SAMLRequest could not be decoded."), 'error');
    drupal_goto();
    return;
  }
  $relay_state = $_GET['RelayState']; // This is the url to send them back towards.
  if ($relay_state === "") {
    drupal_set_message(t("No page to send you to!"), 'error');
    drupal_goto();
    return;
  }
  $interesting = get_the_interesting_parts($SAMLRequest);
  if ($interesting === FALSE) {
    drupal_set_message(t("SAMLRequest could not be decoded: the interesting parts could not be extracted."), 'error');
    drupal_goto();
    return;
  }
  $public = variable_get('googleauth_public_key_path', FALSE);
  if ($public === FALSE) {
    drupal_set_message(t("Unable to find a public key!"), 'error');
    drupal_goto();
    return;
  }
  $private = variable_get('googleauth_private_key_path', FALSE);
  if ($private === FALSE) {
    drupal_set_message(t("Unable to find a private key!"), 'error');
    drupal_goto();
    return;
  }
  $interesting['user_name'] = $user_name;
  $interesting['key_type'] = variable_get('googleauth_key_type', 'dsa');
  $interesting['not_before'] = get_wierd_time(-300000);
  $interesting['not_on_or_after'] = get_wierd_time(600000);
  $output = get_the_actual_response($interesting, $public, $private);
  if ($output === FALSE) {
    drupal_goto();
    return;
  }
  $true_output =<<<EOF
  <form name="acsForm" action="{{ACS_URL}}" method="post">
    <div style="display: none">
    <textarea style="display:none;" rows=10 cols=80 name="SAMLResponse">{{SAMLResponse}}</textarea>
    <textarea style="display:none;" rows=10 cols=80 name="RelayState">{{RelayState}}</textarea>
    </div>
    <input type="submit" value="Click here to authenticate"/>
    </form>
EOF;
  $true_output = str_replace('{{ACS_URL}}', $interesting['acs_url'], $true_output); 
  $true_output = str_replace('{{SAMLResponse}}', $output, $true_output); 
  $true_output = str_replace('{{RelayState}}', $relay_state, $true_output); 
  return $true_output;
}

/**
 * Menu callback which responds to sign out requests.
 */

function googleauth_sign_out() {
  //Function does nothing right now (although it could i.e. log the signout)
  drupal_set_message(t("Signed out of Google Apps"));
  drupal_goto();
}

/**
 * Implementation of hook_perm().
 */

function googleauth_perm() {
	return array('use googleauth', 'administer googleauth');
}

/**
 * Provides a form to get settings from the users
 */

function googleauth_adminpage() {
  $form = array();
  $form['googleauth_key_type'] = array(
    '#type' => 'select',
    '#title' => t('Public/Private Key type'),
    '#default_value' => variable_get('googleauth_key_type', 'dsa'),
    '#options' => array('dsa' => t('dsa'), 'rsa' => t('rsa')),
    '#description' => t("The key generation algorithm."),
    '#required' => TRUE
  );
  $form['googleauth_private_key_path'] = array(
    '#type' => 'textfield',
    '#title' => t('Path to private key'),
    '#default_value' => variable_get('googleauth_private_key_path', ''),
    '#description' => t("The full, absolute path to the private key."),
    '#required' => TRUE
  );
  $form['googleauth_public_key_path'] = array(
    '#type' => 'textfield',
    '#title' => t('Path to public key'),
    '#default_value' => variable_get('googleauth_public_key_path', ''),
    '#description' => t("The full, absolute path to the public key."),
    '#required' => TRUE
  );
  $form['googleauth_path_to_xmlsec'] = array(
    '#type' => 'textfield',
    '#title' => t('Path to xmlsec'),
    '#default_value' => variable_get('googleauth_path_to_xmlsec', '/usr/bin/xmlsec1'),
    '#description' => t("The full, absolute path to xmlsec."),
    '#required' => TRUE
  );
  return system_settings_form($form);
}
