In an
earlier post, I explored the use of the Google Apps Script Content Service supplying data to a web page in an effort to provide a reasonable degree of separation between a service view of Google Apps and a presentation layer. There were also considerable side-benefits in performance and freeing up of the way the web page can be built.
- HTML Service in GAS in comparatively slow delivering even a simple page.
- Adding large libraries like JQUERY and DataTables introduces further delays server-side
- In my simple use of DataTables, simply painting the screen first time was very slow
The issue with JSONP is the exposure of script which can then be captured by a third party who may then induce an authorised user to load another page which includes the script and provides the third party with access to restricted data and controls. Note this does require an active attacker (information cannot accidentally leak).
With any information storage, you do have to consider the value and sensitivity of the information and the effort that a third party will go through to get it and match your response accordingly.
A simple method of providing assurance that the server is sending its data response to the right page (not some other party copy) involves a number of steps.
- The request from the browser should contain a 'hard to guess' additional factor - a 'session key' used by the server script to verify the validity of the request in concert with the Google Account.
- The session key is stored in a cookie on the local machine. The browser operation will prevent a page from a foreign site from obtaining cookies associated with your site.
- Session key is generated by apps script and stored in a User Property.
- The new session key is returned to the user by an alternate path (email, SMS, UIApp) and entered into the original web page to be stored in cookie.
This strategy should be sufficient to negate the exposure of the script that initiates the JSONP exchange. Of course, it does not protect against all other attacks.
Example apps script code
function doGet(request) {
Logger.log(JSON.stringify(request));
var start = new Date().getTime();
// get user to return in content
var user = Session.getActiveUser().getEmail();
// get session property to check with incoming session parameter
var sessionValue = UserProperties.getProperty('session');
Logger.log("got property sessionValue = "+sessionValue + " parameter = "+request.parameter.session);
if (request.parameter.session == sessionValue && request.parameter.session !== undefined ) {
// get the data into an array
var sourceSpreadSheet = SpreadsheetApp.openById("0At0FkhjjhjhjjhjhjjhkhjkhkjZYi15SEpjTkE");
var dataArray = sourceSpreadSheet.getDataRange().getValues();
Logger.log(new Date().getTime() - start);
var aadata = [];
var aoColumns = [];
var col = {};
var html = "";
var headers = dataArray[0];
// all except header for DataTable
for ( var i=1;i<dataArray.length;i++) {
aadata.push(dataArray[i]);
}
// headers for DataTable
for (i=0;i<headers.length;i++) {
col = { "sTitle": headers[i]};
aoColumns.push(col);
}
// result object return by content service
var result ={
"aaData": aadata,
"aoColumns": aoColumns,
"user":user,"success":true
}
}
else {
// not a valid session parameter create new and pass to user through alt path
sessionValue = generateGUID();
// send a mail message (SMS is alternative and arguably more secure but would use a random number for ease of use)
MailApp.sendEmail(user,
"Session Second Factor Security",
"Copy and paste this code into the Session Code Entry Box\n"+sessionValue);
// save it in the userproperties
UserProperties.setProperty('session',sessionValue);
// set return object
var result ={
"aaData": aadata,
"aoColumns": aoColumns,
"user":user, "success":false
}
}
Logger.log(new Date().getTime() - start);
// formatted for JSONP
html = ContentService.createTextOutput(request.parameters.prefix + '(' + JSON.stringify(result) + ')')
.setMimeType(ContentService.MimeType.JSON);
return html;
}
function generateGUID () {
// rfc4122 version 4 compliant solution from broofa
//http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
HTML Code
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" /><title>JQUERY DataTables Page Hosted in Google Drive with cookie for added security</title>
<link rel="stylesheet" type="text/css" href="main.css" />
<link rel="stylesheet" type="text/css" href="//ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/css/jquery.dataTables.css">
<script type="text/javascript" charset="utf8" src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.0.min.js"></script>
<script type="text/javascript" charset="utf8" src="//ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/jquery.dataTables.min.js"></script>
<script type="text/javascript" charset="utf8">$(document).ready(function() {
$('#demo').html( '<table cellpadding="0" cellspacing="0" border="0" class="display" id="example"></table>' );
var proxyJsonp = "https://script.google.com/a/macros/example.com/s/AKfylkjghffdvN2tXR6fo_c/exec";
var sessionparam = "session="+Cookies['ppkcookie1'];
$.getJSON(proxyJsonp + "?" +sessionparam + "&prefix=?", null, function(data) {$('#example').dataTable( {
"aaData": data.aaData ,
"aoColumns": data.aoColumns
} );
document.getElementById("user").innerHTML=data.user;});
} );</script>
<script type="text/javascript" src="cookies.js"></script>
<script type="text/javascript">function saveIt(name) {
var x = document.forms['cookieform'].cookievalue.value;
if (!x)
alert('Please fill in a value in the input box.');
else {
Cookies.create(name,x,7);
alert('Cookie created');
}
}</script>
</head>
<body>
<h1>JQUERY DataTables Page Hosted in Google Drive with cookie for added security</h1>
<div id="header">
</div>
<div id="content">
<h3>
<form name="cookieform" action="#"><p>
Session Code <input name="cookievalue" />
</p></form><p><a href="javascript:saveIt('ppkcookie1')" class="page">Save Session Code</a><br />
</h3>
</div>
<p>Session cookie contains a hard to guess value to be passed in request and validated at server</p>
<p>Data returns from Google Apps Script JSONP</p>
<p>Includes sorting, paging and filtering by default.</p>
<p id="user">User ???????</p>
<p>User must be logged on to Google Apps Account for this data table to appear (by design!) </p>
<p>Entire data table loaded in one hit.</p>
<div id="demo">This iss where the table goes</div></body>
</html>
Cookies usage was based on http://www.quirksmode.org/js/cookies.html