Skip to content

Commit

Permalink
MDL-40848 badges: Validate backpack connection via Persona
Browse files Browse the repository at this point in the history
Instead of trusting that the user owns the email address they
supply, we now require them to login via Persona before
connecting them to the backpack
  • Loading branch information
simoncoggins committed Sep 16, 2013
1 parent 83f26f6 commit f6ebcd3
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 27 deletions.
85 changes: 84 additions & 1 deletion badges/backpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,87 @@ function check_site_access() {
});

return false;
}
}

/*
* Update the status indicator to show connection progress.
*/
function badges_set_connection_progress(status) {
switch (status) {
case 'connecting':
var connecting = M.util.get_string('connecting', 'badges');
var imageurl = M.util.image_url('i/loading_small', 'moodle');
var loading = Y.Node.create(connecting + '&nbsp;<img src="'+imageurl+'" width="16" height="16" alt="'+connecting+'"/>');
Y.one('#connection-status').removeClass('notconnected').addClass('connecting').setHTML(loading);
break;
case 'notconnected':
var notconnected = M.util.get_string('notconnected', 'badges');;
Y.one('#connection-status').removeClass('connecting').addClass('notconnected').setHTML(notconnected);
break;
default:
// Unknown status, do nothing.
}
}

/*
* Print an error message at the top of the page.
*/
function badges_set_error_message(messagekey, param) {
var errortext = M.util.get_string(messagekey, 'badges', param);
Y.one('#connection-error').setHTML(errortext);
}

/*
* Handle the assertion generated by the Persona dialog.
*/
function badges_handle_assertion(assertion) {

if (!assertion) {
var noassertionstr = M.util.get_string('error:noassertion', 'badges');
badges_set_error_message('error:backpackloginfailed', noassertionstr);
return;
}

badges_set_connection_progress('connecting');

Y.io("backpackconnect.php", {
method: "POST",
data: "assertion="+assertion+"&sesskey="+M.cfg.sesskey,
on: {
success: function (id, result) {
// Reload page to display connected email address.
window.location.href = "mybackpack.php";
},
failure: function (id, result) {
try {
var parsedResponse = Y.JSON.parse(result.response);
} catch (e) {
badges_set_connection_progress('notconnected');
var badjsonstr = M.util.get_string('error:badjson', 'badges');
badges_set_error_message('error:backpackloginfailed', badjsonstr);
return;
}
badges_set_connection_progress('notconnected');
badges_set_error_message('error:backpackloginfailed', parsedResponse.reason);
return;
}
}
});
}

/**
* Create and bind the persona login button.
*/
function badges_init_persona_login_button() {
// Create the login button and add to the page via Javascript.
var imageurl = M.util.image_url('i/persona_sign_in_black', 'moodle');
var imagealt = M.util.get_string('signinwithyouremail', 'badges');
var button = Y.Node.create('<img id="persona_signin" src="'+imageurl+'" width="202" height="25" alt="'+imagealt+'"/>');
Y.one('#persona-container').append(button);

// Bind a click event to trigger login when clicked.
button.on('click', function() {
Y.one('#connection-error').empty();
navigator.id.get(badges_handle_assertion);
});
}
51 changes: 28 additions & 23 deletions badges/backpack_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,37 @@ class edit_backpack_form extends moodleform {
* Defines the form
*/
public function definition() {
global $USER;
global $USER, $PAGE, $OUTPUT;
$mform = $this->_form;

$mform->addElement('html', html_writer::tag('span', '', array('class' => 'notconnected', 'id' => 'connection-error')));
$mform->addElement('header', 'backpackheader', get_string('backpackconnection', 'badges'));
$mform->addHelpButton('backpackheader', 'backpackconnection', 'badges');
$mform->addElement('static', 'url', get_string('url'), 'http://backpack.openbadges.org');
$status = html_writer::tag('span', get_string('notconnected', 'badges'), array('class' => 'notconnected'));
$mform->addElement('static', 'url', get_string('url'), BADGE_BACKPACKURL);
$status = html_writer::tag('span', get_string('notconnected', 'badges'),
array('class' => 'notconnected', 'id' => 'connection-status'));
$mform->addElement('static', 'status', get_string('status'), $status);

$mform->addElement('text', 'email', get_string('email'), array('size' => '50'));
$mform->setDefault('email', $USER->email);
$mform->setType('email', PARAM_RAW);
$mform->addRule('email', get_string('required'), 'required', null , 'client');
$mform->addHelpButton('email', 'backpackemail', 'badges');
$nojs = html_writer::tag('noscript', get_string('error:personaneedsjs', 'badges'),
array('class' => 'notconnected'));
$personadiv = $OUTPUT->container($nojs, null, 'persona-container');

$mform->addElement('static', 'persona', '', $personadiv);
$mform->addHelpButton('persona', 'personaconnection', 'badges');

$PAGE->requires->js(new moodle_url('https://login.persona.org/include.js'));
$PAGE->requires->js('/badges/backpack.js');
$PAGE->requires->js_init_call('badges_init_persona_login_button', null, false);
$PAGE->requires->strings_for_js(array('error:backpackloginfailed', 'signinwithyouremail',
'error:noassertion', 'error:connectionunknownreason', 'error:badjson', 'connecting',
'notconnected'), 'badges');

$mform->addElement('hidden', 'userid', $USER->id);
$mform->setType('userid', PARAM_INT);

$mform->addElement('hidden', 'backpackurl', 'http://backpack.openbadges.org');
$mform->addElement('hidden', 'backpackurl', BADGE_BACKPACKURL);
$mform->setType('backpackurl', PARAM_URL);

$this->add_action_buttons(true, get_string('connect', 'badges'));
}

/**
Expand All @@ -70,18 +79,14 @@ public function validation($data, $files) {
global $DB;
$errors = parent::validation($data, $files);

if (!validate_email($data['email'])) {
$errors['email'] = get_string('invalidemail');
} else {
$check = new stdClass();
$check->backpackurl = $data['backpackurl'];
$check->email = $data['email'];

$bp = new OpenBadgesBackpackHandler($check);
$request = $bp->curl_request('user');
if (isset($request->status) && $request->status == 'missing') {
$errors['email'] = get_string('error:nosuchuser', 'badges');
}
$check = new stdClass();
$check->backpackurl = $data['backpackurl'];
$check->email = $data['email'];

$bp = new OpenBadgesBackpackHandler($check);
$request = $bp->curl_request('user');
if (isset($request->status) && $request->status == 'missing') {
$errors['email'] = get_string('error:nosuchuser', 'badges');
}
return $errors;
}
Expand Down Expand Up @@ -113,7 +118,7 @@ public function definition() {

$mform->addElement('header', 'backpackheader', get_string('backpackconnection', 'badges'));
$mform->addHelpButton('backpackheader', 'backpackconnection', 'badges');
$mform->addElement('static', 'url', get_string('url'), 'http://backpack.openbadges.org');
$mform->addElement('static', 'url', get_string('url'), BADGE_BACKPACKURL);

$status = html_writer::tag('span', get_string('connected', 'badges'), array('class' => 'connected'));
$mform->addElement('static', 'status', get_string('status'), $status);
Expand Down
136 changes: 136 additions & 0 deletions badges/backpackconnect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* AJAX script for validating backpack connection.
*
* @package core
* @subpackage badges
* @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Simon Coggins <simon.coggins@totaralms.com>
*/

define('AJAX_SCRIPT', true);

require_once(dirname(dirname(__FILE__)) . '/config.php');
require_once($CFG->dirroot . '/badges/lib/backpacklib.php');
require_once($CFG->libdir . '/filelib.php');

require_sesskey();
require_login();
$PAGE->set_url('/badges/backpackconnect.php');
$PAGE->set_context(context_system::instance());

// Use PHP input filtering as there is no PARAM type for
// the type of cleaning that is required (ASCII chars 32-127 only).
$assertion = filter_input(
INPUT_POST,
'assertion',
FILTER_UNSAFE_RAW,
FILTER_FLAG_STRIP_LOW|FILTER_FLAG_STRIP_HIGH
);

// Audience is the site url scheme + host + port only.
$wwwparts = parse_url($CFG->wwwroot);
$audience = $wwwparts['scheme'] . '://' . $wwwparts['host'];
$audience .= isset($wwwparts['port']) ? $wwwparts['port'] : '';
$params = 'assertion=' . urlencode($assertion) . '&audience=' .
urlencode($audience);

$curl = new curl();
$url = 'https://verifier.login.persona.org/verify';
$options = array(
'FRESH_CONNECT' => true,
'RETURNTRANSFER' => true,
'FORBID_REUSE' => true,
'SSL_VERIFYPEER' => true,
'SSL_VERIFYHOST' => 2,
'HEADER' => 0,
'HTTPHEADER' => array('Content-type: application/x-www-form-urlencoded'),
'CONNECTTIMEOUT' => 0,
'TIMEOUT' => 10, // Fail if data not returned within 10 seconds.
);
$result = $curl->post($url, $params, $options);

// Handle time-out and failed request.
if ($curl->errno != 0) {
if ($curl->errno == CURLE_OPERATION_TIMEOUTED) {
$reason = get_string('error:requesttimeout', 'badges');
} else {
$reason = get_string('error:requesterror', 'badges', $curl->errno);
}
badges_send_response('failure', $reason);
}

$data = json_decode($result);

if (!isset($data->status) || $data->status != 'okay') {
$reason = isset($data->reason) ? $data->reason : get_string('error:connectionunknownreason', 'badges');
badges_send_response('failure', $reason);
}

// Make sure email matches a backpack.
$check = new stdClass();
$check->backpackurl = BADGE_BACKPACKURL;
$check->email = $data->email;

$bp = new OpenBadgesBackpackHandler($check);
$request = $bp->curl_request('user');
if (isset($request->status) && $request->status == 'missing') {
$reason = get_string('error:backpackemailnotfound', 'badges', $data->email);
badges_send_response('failure', $reason);
} else if (empty($request->userId)) {
$reason = get_string('error:backpackdatainvalid', 'badges');
badges_send_response('failure', $reason);
} else {
$backpackuid = $request->userId;
}

// Insert record.
$obj = new stdClass();
$obj->userid = $USER->id;
$obj->email = $data->email;
$obj->backpackurl = BADGE_BACKPACKURL;
$obj->backpackuid = $backpackuid;
$obj->autosync = 0;
$obj->password = '';
$DB->insert_record('badge_backpack', $obj);

// Return success indicator and email address.
badges_send_response('success', $data->email);


/**
* Return a JSON response containing the response provided.
*
* @param string $status Status of the response, typically 'success' or 'failure'.
* @param string $responsetext On success, the email address of the user,
* otherwise a reason for the failure.
* @return void Outputs the JSON and terminates the script.
*/
function badges_send_response($status, $responsetext) {
$out = new stdClass();
$out->status = $status;
if ($status == 'success') {
$out->email = $responsetext;
} else {
$out->reason = $responsetext;
send_header_404();
}
echo json_encode($out);
exit;
}
6 changes: 6 additions & 0 deletions badges/lib/backpacklib.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@

defined('MOODLE_INTERNAL') || die();

/*
* URL of backpack. Currently only the Open Badges backpack
* is supported.
*/
define('BADGE_BACKPACKURL', 'http://backpack.openbadges.org');

global $CFG;
require_once($CFG->libdir . '/filelib.php');

Expand Down
17 changes: 15 additions & 2 deletions lang/en/badges.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@
Currently, only <a href="http://backpack.openbadges.org">Mozilla OpenBadges Backpack</a> is supported. You need to sign up for a backpack service before trying to set up backpack connection on this page.';
$string['backpackdetails'] = 'Backpack settings';
$string['backpackemail'] = 'Email address';
$string['backpackemail_help'] = 'Email address associated with your backpack.
$string['backpackemail_help'] = 'The email address associated with your backpack. While you are connected, any badges earned on this site will be associated with this email address.';
$string['personaconnection'] = 'Sign in with your email';
$string['personaconnection_help'] = 'Persona is a system for identifying yourself across the web, using an email address that you own. The Open Badges backpack uses Persona as a login system, so to be able to connect to a backpack you with need a Persona account.
If backpack connection is established, this email address is used instead of your internal email address to push badges to your backpack.';
For more information about Persona visit <a href="https://login.persona.org/about">https://login.persona.org/about</a>.';
$string['backpackimport'] = 'Badge import settings';
$string['backpackimport_help'] = 'After backpack connection is successfully established, badges from your backpack can be displayed on your "My Badges" page and your profile page.
Expand Down Expand Up @@ -128,6 +130,7 @@
$string['configuremessage'] = 'Badge message';
$string['connect'] = 'Connect';
$string['connected'] = 'Connected';
$string['connecting'] = 'Connecting...';
$string['contact'] = 'Contact';
$string['contact_help'] = 'An email address associated with the badge issuer.';
$string['copyof'] = 'Copy of {$a}';
Expand Down Expand Up @@ -199,10 +202,15 @@
$string['donotaward'] = 'Currently, this badge is not active, so it cannot be awarded to users. If you would like to award this badge, please set its status to active.';
$string['editsettings'] = 'Edit settings';
$string['enablebadges'] = 'Enable badges';
$string['error:backpackdatainvalid'] = 'The data return from the backpack was invalid.';
$string['error:backpackemailnotfound'] = 'The email \'{$a}\' is not associated with a backpack. You need to <a href="http://backpack.openbadges.org">create a backpack</a> for that account or sign in with another email address.';
$string['error:backpacknotavailable'] = 'Your site is not accessible from the Internet, so any badges issued from this site cannot be verified by external backpack services.';
$string['error:backpackloginfailed'] = 'You could not be connected to an external backpack for the following reason: {$a}';
$string['error:backpackproblem'] = 'There was a problem connecting to your backpack service provider. Please try again later.';
$string['error:badjson'] = 'The connection attempt returned invalid data.';
$string['error:cannotact'] = 'Cannot activate the badge. ';
$string['error:cannotawardbadge'] = 'Cannot award badge to a user.';
$string['error:connectionunknownreason'] = 'The connection was unsuccessful but no reason was given.';
$string['error:clone'] = 'Cannot clone the badge.';
$string['error:duplicatename'] = 'Badge with such name already exists in the system.';
$string['error:externalbadgedoesntexist'] = 'Badge not found';
Expand All @@ -211,6 +219,7 @@
$string['error:invalidexpiredate'] = 'Expiry date has to be in the future.';
$string['error:invalidexpireperiod'] = 'Expiry period cannot be negative or equal 0.';
$string['error:noactivities'] = 'There are no activities with completion criteria enabled in this course.';
$string['error:noassertion'] = 'No assertion was returned by Persona. You may have closed the dialog before completing the login process.';
$string['error:nocourses'] = 'Course completion is not enabled for any of the courses in this site, so none can be displayed. Course completion may be enabled in the course settings.';
$string['error:nogroups'] = '<p>There are no public collections of badges available in your backpack. </p>
<p>Only public collections are shown, <a href="http://backpack.openbadges.org">visit your backpack</a> to create some public collections.</p>';
Expand All @@ -223,6 +232,9 @@
$string['error:nosuchuser'] = 'User with this email address does not have an account with the current backpack provider.';
$string['error:notifycoursedate'] = 'Warning: Badges associated with course and activity completions will not be issued until the course start date.';
$string['error:parameter'] = 'Warning: At least one parameter should be selected to ensure correct badge issuing workflow.';
$string['error:personaneedsjs'] = 'Currently, Javascript is required to connect to your backpack. If you can, enable Javascript and reload the page.';
$string['error:requesttimeout'] = 'The connection request timed out before it could complete.';
$string['error:requesterror'] = 'The connection request failed (error code {$a}).';
$string['error:save'] = 'Cannot save the badge.';
$string['evidence'] = 'Evidence';
$string['existingrecipients'] = 'Existing badge recipients';
Expand Down Expand Up @@ -323,6 +335,7 @@
$string['selectgroup_start'] = 'Select collections from your backpack to display on this site:';
$string['selecting'] = 'With selected badges...';
$string['setup'] = 'Set up connection';
$string['signinwithyouremail'] = 'Sign in with your email';
$string['sitebadges'] = 'Site badges';
$string['sitebadges_help'] = 'Site badges can only be awarded to users for site-related activities. These include completing a set of courses or parts of user profiles. Site badges can also be issued manually by one user to another.
Expand Down
Binary file added pix/i/persona_sign_in_black.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions theme/base/style/core.css
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,7 @@ div.badge .expireimage { width: 100px; height: 100px; left: 20px; top: 0px; }
.statusbox.inactive { background-color: #FFEBA8; }
.activatebadge { margin: 0px; text-align: left; vertical-align: middle; }
.addcourse { float: right; }
img#persona_signin { cursor: pointer; }

/**
* The date selector popup.
Expand Down
Loading

0 comments on commit f6ebcd3

Please sign in to comment.