From e1a5a3559029395ec772f2b1ce9966e55fde7c0d Mon Sep 17 00:00:00 2001 From: William Panting Date: Fri, 7 Dec 2012 19:15:46 -0400 Subject: [PATCH 1/3] Token authentication. --- includes/islandora_authtokens.inc | 107 ++++++++++++++++++++++++++++++ islandora.install | 80 ++++++++++++++++++++-- islandora.module | 57 ++++++++++++++-- 3 files changed, 235 insertions(+), 9 deletions(-) create mode 100644 includes/islandora_authtokens.inc diff --git a/includes/islandora_authtokens.inc b/includes/islandora_authtokens.inc new file mode 100644 index 00000000..7acdb898 --- /dev/null +++ b/includes/islandora_authtokens.inc @@ -0,0 +1,107 @@ +fields( + array( + 'token' => $token, + 'uid' => $user->uid, + 'pid' => $pid, + 'dsid' => $dsid, + 'time' => $time, + 'remaining_uses' => $uses, + ))->execute(); + + return $token; +} + +/** + * Submit a token to islandora for authentication. Supply islandora with the + * token and the object/datastream it is for and you will receive access if + * authentication passes. Tokens can only be redeemed in a short window after + * their creation. + * + * @param string $pid + * The PID of the object to retrieve. + * @param string $dsid + * The datastream id to retrieve. + * @param string $token + * The registered token that allows access to this object. + * + * @return mixed + * The user credentials for access if the token validation passes, + * FALSE otherwise + */ +function islandora_validate_object_token($pid, $dsid, $token) { + // Check for database token. + $time = time(); + $query = db_select('islandora_authtokens', 'tokens'); + $query->join('users', 'u', 'tokens.uid = u.uid'); + // The results will look like user objects. + $result = $query + ->fields('u', array('uid', 'name', 'pass')) + ->fields('tokens', array('remaining_uses')) + ->condition('token', $token, '=') + ->condition('pid', $pid, '=') + ->condition('dsid', $dsid, '=') + ->condition('time', $time, '<=') + ->condition('time', $time - TOKEN_TIMEOUT, '>') + ->execute() + ->fetchAll(); + if ($result) { + $remaining_uses = $result[0]->remaining_uses; + $remaining_uses--; + // Remove the authentication token so it can't be used again. + if ($remaining_uses == 0) { + db_delete("islandora_authtokens") + ->condition('token', $token, '=') + ->condition('pid', $pid, '=') + ->condition('dsid', $dsid, '=') + ->execute(); + } + // Decrement authentication token uses. + else { + db_update("islandora_authtokens") + ->fields(array('remaining_uses' => $remaining_uses)) + ->condition('token', $token, '=') + ->condition('pid', $pid, '=') + ->condition('dsid', $dsid, '=') + ->execute(); + } + unset($result[0]->remaining_uses); + return $result[0]; + } + else { + return FALSE; + } +} diff --git a/islandora.install b/islandora.install index 88549ce5..549f7e14 100644 --- a/islandora.install +++ b/islandora.install @@ -1,15 +1,13 @@ 'The hub for all islandora authentication tokens', + 'fields' => array( + 'id' => array( + 'description' => 'key', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'token' => array( + 'description' => 'a unique identifier for this token', + 'type' => 'varchar', + 'length' => 64, + ), + 'remaining_uses' => array( + 'description' => 'How many uses until this should be removed', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'uid' => array( + 'description' => 'the user id that requested this token', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'time' => array( + 'description' => 'when this token was created', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'pid' => array( + 'description' => 'the pid of the object this token unlocks', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + ), + 'dsid' => array( + 'description' => 'the datasteram id of the object this token unlocks', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + ), + 'unique keys' => array( + 'id' => array('id'), + ), + 'primary key' => array('id'), + ); + return $schema; +} + +/** + * Implements hook_update_N(). + * + * Add the required table for handling authentication tokens. + * This is the first instance that has this table. + */ +function islandora_update_7001(&$sandbox) { + drupal_install_schema('islandora'); + $t = get_t(); + return $t("Islandora database updates complete"); +} diff --git a/islandora.module b/islandora.module index 2b553da8..797e183f 100644 --- a/islandora.module +++ b/islandora.module @@ -161,10 +161,13 @@ function islandora_menu() { 'access arguments' => array(FEDORA_VIEW, 2, 4), 'load arguments' => array(2), ); - $items['islandora/object/%islandora_object/datastream/%islandora_datastream/view'] = array( + // This menu item uses token authentication in islandora_tokened_object. + $items['islandora/object/%islandora_tokened_object/datastream/%islandora_tokened_datastream/view'] = array( 'title' => 'View datastream', + 'load arguments' => array('%map'), 'type' => MENU_DEFAULT_LOCAL_TASK, ); + $items['islandora/object/%islandora_object/datastream/%islandora_datastream/download'] = array( 'title' => 'Download datastream', 'page callback' => 'islandora_download_datastream', @@ -518,6 +521,50 @@ function islandora_object_load($object_id) { return NULL; } +/** + * A helper function to get a connection and return an object using a token + * for authentication. + * + * @param string $object_id + * The PID of an object in the menu path identified by + * '%islandora_tokened_object'. + * @param array $map + * Used to extract the Fedora object's DSID at $map[4]. + * + * @return FedoraObject + * A token authenticated object. @see islandora_object_load + */ +function islandora_tokened_object_load($object_id, $map) { + if (array_key_exists('token', $_GET)) { + $token = $_GET['token']; + if ($token) { + module_load_include('inc', 'islandora', 'includes/islandora_authtokens'); + $token_user = islandora_validate_object_token($object_id, $map[4], $token); + islandora_get_tuque_connection($user = $token_user); + } + } + return islandora_object_load($object_id); +} + +/** + * This datastream load must take in arguments in a different + * order than the usual islandora_datastream_load. This is because + * the function islandora_tokened_object_load needs DSID. It uses + * the path %map to avoid duplicate parameters. + * + * @param mixed $datastream_id + * %islandora_tokened_datastream @see islandora_datastream_load + * @param array $map + * Used to extract the Fedora object's PID at $map[2]. + * + * @return FedoraDatastream + * A datastream from Fedora. + * @see islandora_datastream_load + */ +function islandora_tokened_datastream_load($datastream_id, $map) { + return islandora_datastream_load($datastream_id, $map[2]); +} + /** * A helper function to get an datastream specified as '%islandora_datastream' * for the object specified in the menu path as '%islandora_object'. @@ -526,11 +573,13 @@ function islandora_object_load($object_id) { * drupal_access_denied() when appropriate. * * @param string $datastream_id - * The dsid of the datastream specified as '%islandora_datastream' to fetch + * The DSID of the datastream specified as '%islandora_datastream' to fetch * from the given object in the menu path identified by '%islandora_object'. * - * $param string $object_id - * The object to load the datastream from. + * @param mixed $object_id + * The object to load the datastream from. This can be a Fedora PID or + * an instantiated IslandoraFedoraObject as it implements __toString() + * returning the PID. * * @return FedoraDatastream * If the given datastream ID exists then this returns a FedoraDatastream From bf3945ecf84e5f1b226b66b350726d3fdd35ddac Mon Sep 17 00:00:00 2001 From: William Panting Date: Mon, 10 Dec 2012 09:36:01 -0400 Subject: [PATCH 2/3] more secure authentication tokens and better docs --- includes/islandora_authtokens.inc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/includes/islandora_authtokens.inc b/includes/islandora_authtokens.inc index 7acdb898..44b02578 100644 --- a/includes/islandora_authtokens.inc +++ b/includes/islandora_authtokens.inc @@ -6,8 +6,9 @@ * such as Djatoka that do not pass through credentials. */ -// Token lifespan: after this duration the token expires. -define('TOKEN_TIMEOUT', 30000); +// Token lifespan(seconds): after this duration the token expires. +// 5 minutes. +define('TOKEN_TIMEOUT', 300); /** * Request Islandora to construct an object/datastream authentication token. @@ -30,7 +31,11 @@ define('TOKEN_TIMEOUT', 30000); function islandora_get_object_token($pid, $dsid, $uses = 1) { global $user; $time = time(); - $token = hash("sha256", mt_rand()); + // The function mt_rand is not considered cryptographically secure + // and openssl_rando_pseudo_bytes() is only available in PHP > 5.3. + // We might be safe in this case because mt_rand should never be using + // the same seed, but this is still more secure. + $token = hash("sha256", mt_rand() . $time); $id = db_insert("islandora_authtokens")->fields( array( From b25d691d8da3d0e759d54e557d066d7dc81180d8 Mon Sep 17 00:00:00 2001 From: William Panting Date: Mon, 10 Dec 2012 10:18:22 -0400 Subject: [PATCH 3/3] more secure, more documented tokens --- islandora.module | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/islandora.module b/islandora.module index 797e183f..388b2895 100644 --- a/islandora.module +++ b/islandora.module @@ -536,7 +536,7 @@ function islandora_object_load($object_id) { */ function islandora_tokened_object_load($object_id, $map) { if (array_key_exists('token', $_GET)) { - $token = $_GET['token']; + $token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING); if ($token) { module_load_include('inc', 'islandora', 'includes/islandora_authtokens'); $token_user = islandora_validate_object_token($object_id, $map[4], $token); @@ -550,7 +550,12 @@ function islandora_tokened_object_load($object_id, $map) { * This datastream load must take in arguments in a different * order than the usual islandora_datastream_load. This is because * the function islandora_tokened_object_load needs DSID. It uses - * the path %map to avoid duplicate parameters. + * the path %map to avoid duplicate parameters. The menu system + * passes 'load arguments' to both islandora_tokened_object_load + * and this function and the first parameter is positional with the token. + * An alternative: + * islandora_tokened_object_load(PID, DSID, PID) + * islandora_tokened_datastream_load(DSID, DSID, PID) * * @param mixed $datastream_id * %islandora_tokened_datastream @see islandora_datastream_load