Commit 2d93d2ee authored by Aaron Wells's avatar Aaron Wells

Bug 1620879: Adding user self-service token gen scripts

behatnotneeded: Test to come later

Change-Id: I0c1b2b7ee9cc927a23c498293da0ddc1ec31f31e
parent 9345d3e5
......@@ -1686,6 +1686,8 @@ function ensure_user_account_is_active($user=null) {
throw new AccessTotallyDeniedException(get_string('accesstotallydenied_institution' . $state, 'mahara', $authinstance->displayname, $sitename));
return false;
}
return true;
}
/**
......
......@@ -280,10 +280,10 @@ $string['errorunexpectedkey'] = 'Unexpected keys (%s) detected in parameter arra
$string['execute'] = 'Execute';
$string['expires'] = 'Expires';
$string['externalservice'] = 'External service';
$string['failedtolog'] = 'Failed to login';
$string['function'] = 'Function';
$string['generalstructure'] = 'General structure';
$string['information'] = 'Information';
$string['invalidlogin'] = 'Failed to login; check username and password';
$string['invalidaccount'] = 'Invalid web services account: Check service user configuration';
$string['invalidextparam'] = 'Invalid external API parameter: %s';
$string['invalidextresponse'] = 'Invalid external API response: %s';
......@@ -385,3 +385,5 @@ $string['groupnotexist'] = 'Group "%s" does not exist';
$string['instmustset'] = 'institution must be set for "%s"';
$string['nogroup'] = 'no group specified';
$string['membersinvalidaction'] = 'invalid action "%s" for user "%s" on group "%s"';
$string['passwordmustbechangedviawebsite'] = 'You need to change your password. Please log in to you Mahara site in a web browser in order to update your password.';
$string['featuredisabled'] = 'This web services feature has not been enabled for this site. Please contact your site administrator for more information.';
......@@ -480,7 +480,7 @@ function contextualHelp(formName, helpName, pluginType, pluginName, page, sectio
// create and display the container
contextualHelpContainer = jQuery(
'<div style="position: absolute" class="contextualHelp hidden" role="dialog">' +
'<span class="icon icon-spinner icon-pulse"' +
'<span class="icon icon-spinner icon-pulse"></span>' +
'</div>'
);
var container = contextualHelpLink.parent();
......
<?php
/**
* JSON-based user token self-generation script. (Based on Moodle's
* /login/token.php script). Because this requires the password to be
* sent in plaintext, you should only call it over SSL. It's also
* recommended to send the password, at least, as POST data, so
* it doesn't accidentally get printed into any server logs.
*
* @param string username Mahara username of user requesting token
* @param string password Plaintext password of user (you should only
* send this via POST and over SSL)
* @param string clientname (Optional) Human-readable description of the app
* Displayed on the user's "authorized apps" list
* @param string clientenv (Optional) Human-readable description of app's
* environment (e.g.: Android LG-10). Also displayed on the "authorized apps"
* list.
* @param string clientguid (Optional) A globally unique identifier for this
* client. Can be used to make sure this client doesn't trample on the tokens
* used by other instances of the same client.
* @returns object On success, a JSON object with a "token" field. On error,
* a JSON object with an "error" field and additional fields describing the
* error.
*
* @package mahara
* @subpackage webservice
* @author Dongsheng Cai <dongsheng@moodle.com>
* @author Catalyst IT Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright 2011 Dongsheng Cai <dongsheng@moodle.com>
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*/
define('INTERNAL', 1);
define('JSON', 1);
define('NOSESSKEY', 1);
define('PUBLIC', 1);
require(dirname(dirname(dirname(dirname(__FILE__)))) . '/init.php');
require_once(get_config('docroot') . 'webservice/lib.php');
safe_require('module', 'mobileapi');
// you must use HTTPS as token based auth is a hazzard without it
if (!is_https() && get_config('productionmode')) {
header("HTTP/1.0 403 Forbidden - HTTPS must be used");
die;
}
if (!PluginModuleMobileapi::is_service_ready()) {
throw new WebserviceException(
'featuredisabled',
// In production mode we don't want to give too many configuration details
// to not-yet-authorised users.
($CFG->productionmode ? '' : 'The site administrator needs to go to'
. ' "Extensions -> Plugin administration -> module/mobileapi" and enable '
. ' the mobileapi module.'),
501
);
}
// Allow CORS requests.
header('Access-Control-Allow-Origin: *');
$username = param_variable('username');
$password = param_variable('password');
// Which service we'll generate a token for
// TODO: turn this into a system available to other plugins?
// For now I'll hard-code it to only work with this one service.
$serviceshortname = 'maharamobile'; //param_variable('service');
$servicecomponent = 'module/mobileapi'; //param_variable('component');
// Information to describe the client requesting the token
// (To help users understand the token management screen.)
$clientname = param_variable('clientname', '');
$clientenv = param_variable('clientenv', '');
$clientguid = param_variable('clientguid', '');
// Check for max login attempts so we can give a specific message about that.
$logintries = (int) get_field(
'usr',
'logintries',
'username',
$username
);
if ($logintries >= MAXLOGINTRIES) {
throw new WebserviceException(
'toomanyloginfailures',
get_string('toomanytries', 'auth'),
403
);
}
if (!$USER->login($username, $password)) {
throw new WebserviceException(
'invalidlogin',
get_string('loginfailed', 'mahara'),
403
);
}
try {
// This can either die or throw an AccessTotallyDeniedException
// and/or maybe even return false!
$result = ensure_user_account_is_active();
$e = false;
}
catch (AccessTotallyDeniedException $e) {
$result = false;
}
if (!$result) {
throw new WebserviceException(
'accountinactive',
($e ? $e->getMessage() : ''),
403
);
}
if ($USER->get('passwordchange')) {
throw new WebserviceException(
'passwordchangerequired',
'The user needs to reset their password. They must log in to the site through a web browser to do this.',
403
);
}
// Process the token request. (Will throw an exception if the user doesn't
// have access; just let that kill the request if so.)
$token = webservice_user_token_selfservice($serviceshortname, $servicecomponent, $clientname, $clientenv, $clientguid);
$usertoken = new stdClass();
$usertoken->token = $token;
echo json_encode($usertoken);
<?php
/**
* iframe-based user token self-generation script. (Based on Moodle's
* /local/mobile/launch.php script).
*
* and from there they should be able to log in via standard or SSO auth.
* Once done, they'll be redirected back to this page, which will place
* the token into a Javascript variable, that your app can then read.
*
* Note this won't (shouldn't) work if addressed directly in a normal
* web browser, because of CORS restrictions. Webviews, however, are
* exempt from some of the CORS rules.
*
* Because the user will be authenticating, this should only be called
* over HTTPS.
*
* @param string clientname (Optional) Human-readable description of the app
* Displayed on the user's "authorized apps" list
* @param string clientenv (Optional) Human-readable description of app's
* environment (e.g.: Android LG-10). Also displayed on the "authorized apps"
* list.
* @param string clientguid (Optional) A globally unique identifier for this
* client. Can be used to make sure this client doesn't trample on the tokens
* used by other instances of the same client.
* @return Declares a JSON array called "mahara_token_response". Client
*
* @package mahara
* @subpackage webservice
* @author Juan Leyva <juan@moodle.com>
* @author Catalyst IT Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright 2014 Juan Leyva <juan@moodle.com>
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*/
define('INTERNAL', 1);
define('NOSESSKEY', 1);
define('PUBLIC', 1);
require_once(dirname(dirname(dirname(__FILE__))) . '/init.php');
require_once(get_config('docroot'). 'webservice/lib.php');
safe_require('module', 'mobileapi');
// you must use HTTPS as token based auth is a hazzard without it
if (!is_https() && get_config('productionmode')) {
header("HTTP/1.0 403 Forbidden - HTTPS must be used");
die;
}
if (!PluginModuleMobileapi::is_service_ready()) {
throw new WebserviceException(
'featuredisabled',
// In production mode we don't want to give too many configuration details
// to not-yet-authorised users.
($CFG->productionmode ? '' : 'The site administrator needs to go to'
. ' "Extensions -> Plugin administration -> module/mobileapi" and enable '
. ' the mobileapi module.'),
501
);
}
// Which service we'll generate a token for
// TODO: turn this into a system available to other plugins?
// For now I'll hard-code it to only work with this one service.
$serviceshortname = 'maharamobile'; //param_variable('service');
$servicecomponent = 'module/mobileapi'; //param_variable('component');
// Information to describe the client requesting the token
// (To help users understand the token management screen.)
$clientname = param_variable('clientname', '');
$clientenv = param_variable('clientenv', '');
$clientguid = param_variable('clientguid', '');
// TODO: An identifier to send back to the requestor, like Moodle uses?
//$passport = param_variable('passport');
// Send the user to the login screen; they'll be sent back here when
// they authenticate correctly.
if (!$USER->is_logged_in()) {
$redirect = get_relative_script_path();
if (!preg_match('/[&?]login/', $redirect)) {
// Add "login" query param to URL
$redirect =
$redirect
// Check if there are existing params, or if this is the first one.
. ((strpos($redirect, '?') !== false) ? '&' : '?')
. 'login';
}
redirect($redirect);
exit();
}
// Process the token request.
$token = webservice_user_token_selfservice($serviceshortname, $servicecomponent, $clientname, $clientenv, $clientguid);
// TODO: Include passport in response like Moodle does?
//$usertoken->passport = $passport;
// Using smarty_core() instead of smarty() because smarty() computes a lot
// of things we don't need here.
$smarty = smarty_core();
$smarty->assign('STYLESHEETLIST', get_stylesheets_for_current_page(array(), array()));
$smarty->assign('SERIES', get_config('series'));
$smarty->assign('token', $token);
$smarty->display('module:mobileapi:tokenform.tpl');
<!doctype html>
<head>
<meta name="generator" content="Mahara {$SERIES} (https://mahara.org)" />
<meta http-equiv="Content-type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<script type="application/javascript">
window.onload = function() {
console.log("Running: {$token}");
window.maharatoken = "{$token}";
}
</script>
{foreach from=$STYLESHEETLIST item=cssurl}
<link rel="stylesheet" type="text/css" href="{$cssurl}">
{/foreach}
</head>
<body>
<!-- TODO: Put some nice "Redirecting you..." placeholder here? -->
<span class="icon icon-spinner icon-pulse"></span>
</body>
\ No newline at end of file
......@@ -2316,3 +2316,158 @@ class WebserviceAccessException extends WebserviceException {
parent::__construct('accessexception', $debuginfo);
}
}
/**
* Process the logged-in user's REST-based request for a webservices token.
* Checks whether the user has permission to self-generate a token for the
* requested service group. Then it issues a new token, or retrieves an
* existing one if the user already has an applicable token.
*
* @param string $serviceshortname Shortname of the desired service group
* @param string $servicecomponent The service group's component
* @param string $clientname (Optional) Human-readable name of client program using this token
* @param string $clientenv (Optional) Human-readable description of device/environment for client
* @param string $clientguid (Optional) Unique identifier for the client program
* @throws WebserviceException
* @return string The token generated
*/
function webservice_user_token_selfservice($serviceshortname, $servicecomponent, $clientname, $clientenv, $clientguid) {
global $USER;
// TODO: more granular access controls: Is this user allowed to access webservices at all?
// From here, we know that the user has at least logged in, so we can
// expose a little bit more information in the error responses.
$service = get_record('external_services', 'shortname', $serviceshortname, 'component', $servicecomponent);
if (empty($service)) {
// will throw exception if no token found
throw new WebserviceException(
'servicenotfound',
"No service group found with name $servicecomponent/$serviceshortname",
400
);
}
else if (!$service->enabled) {
throw new WebserviceException(
'servicenotenabled',
'Requested service group is disabled.',
501
);
}
// TODO: more granular access controls: Is this user allowed to access this particular service group?
//specific checks related to user restricted service
if ($service->restrictedusers) {
$authoriseduser = get_record(
'external_services_users',
'externalserviceid', $service->id,
'userid', $USER->get('id')
);
if (empty($authoriseduser)) {
throw new WebserviceException(
'usernotauthorised',
'This service is restricted to authorized users only.',
403
);
}
if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) {
throw new WebserviceException(
'userauthorisationexpired',
'Your access rights to this service have expired.',
403
);
}
require_once(get_config('docroot') . 'webservice/libs/net.php');
if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) {
throw new WebserviceException(
'ipnotauthorised',
'This service is restricted to authorized IP ranges only.',
403
);
}
}
// Check if a token has already been created for this user and this service
$tokensql = "SELECT t.id, t.sid, t.token, t.validuntil, t.iprestriction
FROM {external_tokens} t
WHERE t.userid = ? AND t.externalserviceid = ? AND t.tokentype = ?";
$tokenparams = array(
$USER->get('id'),
$service->id,
EXTERNAL_TOKEN_USER
);
// Client specified a GUID; so only re-use that same token.
if ($clientname || $clientguid) {
$tokensql .= ' AND clientname = ? AND clientguid = ? ';
$tokenparams[] = $clientname;
$tokenparams[] = $clientguid;
}
$tokensql .= ' ORDER BY t.ctime ASC';
$tokens = get_records_sql_array($tokensql, $tokenparams);
if (!$tokens) {
$tokens = array();
}
//A bit of sanity checks
foreach ($tokens as $key=>$token) {
/// Checks related to a specific token. (script execution continue)
$unsettoken = false;
// Take this opportunity to delete expired tokens
// (similar logic to the web service servers
// /webservice/lib.php/webservice_server::authenticate_by_token())
if (!empty($token->validuntil) and $token->validuntil < time()) {
delete_records('external_tokens', 'id', $token->id);
$unsettoken = true;
}
// remove token if its ip not in whitelist
if (isset($token->iprestriction) and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
$unsettoken = true;
}
if ($unsettoken) {
unset($tokens[$key]);
}
}
// if some valid tokens exist then use the most recent
if (count($tokens) > 0) {
// Retrieve an existing token
$token = array_pop($tokens);
// log token access
set_field(
'external_tokens',
'mtime',
db_format_timestamp(time()),
'id',
$token->id
);
$token = $token->token;
}
else {
// Generate a new token
// If you wanted to separately restrict the ability to *generate*
// a token, (as opposed to just retrieving one), this would be the
// place to do it.
$token = webservice_generate_token(
EXTERNAL_TOKEN_USER,
$service,
$USER->get('id'), // token user
null, // institution
(time() + EXTERNAL_TOKEN_USER_EXPIRES), //expiration
null, // iprestriction
$clientname,
$clientenv,
$clientguid
);
}
return $token;
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment