NIFI-655:

- Adding automatic client side token renewal.
This commit is contained in:
Matt Gilman 2015-11-13 16:19:05 -05:00
parent 06cb7cfc10
commit 749f4e9be1
12 changed files with 1117 additions and 978 deletions

View File

@ -28,6 +28,7 @@
<link rel="stylesheet" href="js/jquery/qtip2/jquery.qtip.min.css?" type="text/css" /> <link rel="stylesheet" href="js/jquery/qtip2/jquery.qtip.min.css?" type="text/css" />
<link rel="stylesheet" href="js/jquery/ui-smoothness/jquery-ui-1.10.4.min.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/ui-smoothness/jquery-ui-1.10.4.min.css" type="text/css" />
<script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
<script type="text/javascript" src="js/jquery/jquery.center.js"></script> <script type="text/javascript" src="js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script>
<script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script>

View File

@ -40,6 +40,7 @@
<script type="text/javascript" src="js/codemirror/lib/codemirror-compressed.js"></script> <script type="text/javascript" src="js/codemirror/lib/codemirror-compressed.js"></script>
<script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/jquery/ui-smoothness/jquery-ui-1.10.4.min.js"></script> <script type="text/javascript" src="js/jquery/ui-smoothness/jquery-ui-1.10.4.min.js"></script>
<script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
<script type="text/javascript" src="js/jquery/jquery.center.js"></script> <script type="text/javascript" src="js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="js/jquery/jquery.ellipsis.js"></script> <script type="text/javascript" src="js/jquery/jquery.ellipsis.js"></script>
<script type="text/javascript" src="js/jquery/jquery.each.js"></script> <script type="text/javascript" src="js/jquery/jquery.each.js"></script>

View File

@ -30,6 +30,7 @@
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" />
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" />
<script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
<script type="text/javascript" src="js/jquery/jquery.center.js"></script> <script type="text/javascript" src="js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script>
<script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script>

View File

@ -30,6 +30,7 @@
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" />
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" />
<script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
<script type="text/javascript" src="js/jquery/jquery.center.js"></script> <script type="text/javascript" src="js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script>
<script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script>

View File

@ -30,6 +30,7 @@
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" />
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" />
<script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
<script type="text/javascript" src="js/jquery/jquery.center.js"></script> <script type="text/javascript" src="js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script>
<script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script>

View File

@ -31,6 +31,7 @@
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" />
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" />
<script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
<script type="text/javascript" src="js/jquery/jquery.center.js"></script> <script type="text/javascript" src="js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script>
<script type="text/javascript" src="js/jquery/tabbs/jquery.tabbs.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/tabbs/jquery.tabbs.js?${project.version}"></script>

View File

@ -38,6 +38,7 @@
<script type="text/javascript" src="js/codemirror/lib/codemirror-compressed.js"></script> <script type="text/javascript" src="js/codemirror/lib/codemirror-compressed.js"></script>
<script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/jquery/ui-smoothness/jquery-ui-1.10.4.min.js"></script> <script type="text/javascript" src="js/jquery/ui-smoothness/jquery-ui-1.10.4.min.js"></script>
<script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
<script type="text/javascript" src="js/jquery/jquery.center.js"></script> <script type="text/javascript" src="js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="js/jquery/tabbs/jquery.tabbs.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/tabbs/jquery.tabbs.js?${project.version}"></script>
<script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script>

View File

@ -30,6 +30,7 @@
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" />
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" />
<script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
<script type="text/javascript" src="js/jquery/jquery.center.js"></script> <script type="text/javascript" src="js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="js/jquery/jquery.form.min.js"></script> <script type="text/javascript" src="js/jquery/jquery.form.min.js"></script>
<script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script>

View File

@ -31,6 +31,7 @@
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick.grid.css" type="text/css" />
<link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" /> <link rel="stylesheet" href="js/jquery/slickgrid/css/slick-default-theme.css" type="text/css" />
<script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
<script type="text/javascript" src="js/jquery/jquery.center.js"></script> <script type="text/javascript" src="js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="js/jquery/tabbs/jquery.tabbs.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/tabbs/jquery.tabbs.js?${project.version}"></script>
<script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script> <script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script>

View File

@ -98,7 +98,7 @@ nf.Login = (function () {
} }
}).done(function (jwt) { }).done(function (jwt) {
// store the jwt and reload the page // store the jwt and reload the page
nf.Storage.setItem('jwt', jwt); nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt));
// check to see if they actually have access now // check to see if they actually have access now
$.ajax({ $.ajax({
@ -109,15 +109,18 @@ nf.Login = (function () {
if (response.identity === 'anonymous') { if (response.identity === 'anonymous') {
showLogoutLink(); showLogoutLink();
// schedule automatic token refresh
nf.Common.scheduleTokenRefresh();
// show the user // show the user
var user = getJwtSubject(jwt); var user = nf.Common.getJwtSubject(jwt);
$('#nifi-user-submit-justification').text(user); $('#nifi-user-submit-justification').text(user);
// show the registration form // show the registration form
initializeNiFiRegistration(); initializeNiFiRegistration();
showNiFiRegistration(); showNiFiRegistration();
} else { } else {
// reload as appropriate // reload as appropriate - no need to schedule token refresh as the page is reloading
if (top !== window) { if (top !== window) {
parent.window.location = '/nifi'; parent.window.location = '/nifi';
} else { } else {
@ -127,8 +130,11 @@ nf.Login = (function () {
}).fail(function (xhr, status, error) { }).fail(function (xhr, status, error) {
showLogoutLink(); showLogoutLink();
// schedule automatic token refresh
nf.Common.scheduleTokenRefresh();
// show the user // show the user
var user = getJwtSubject(jwt); var user = nf.Common.getJwtSubject(jwt);
$('#nifi-user-submit-justification').text(user); $('#nifi-user-submit-justification').text(user);
if (xhr.status === 401) { if (xhr.status === 401) {
@ -189,33 +195,6 @@ nf.Login = (function () {
}); });
}; };
/**
* Extracts the subject from the specified jwt. If the jwt is not as expected
* an empty string is returned.
*
* @param {string} jwt
* @returns {string}
*/
var getJwtSubject = function (jwt) {
if (nf.Common.isDefinedAndNotNull(jwt)) {
var segments = jwt.split(/\./);
if (segments.length !== 3) {
return '';
}
var rawPayload = $.base64.atob(segments[1]);
var payload = JSON.parse(rawPayload);
if (nf.Common.isDefinedAndNotNull(payload['preferred_username'])) {
return payload['preferred_username'];
} else {
'';
}
}
return '';
};
var logout = function () { var logout = function () {
nf.Storage.removeItem('jwt'); nf.Storage.removeItem('jwt');
}; };
@ -272,7 +251,7 @@ nf.Login = (function () {
$('#login-message').text('Your account is active and you are already logged in.'); $('#login-message').text('Your account is active and you are already logged in.');
}).fail(function (xhr, status, error) { }).fail(function (xhr, status, error) {
if (xhr.status === 401) { if (xhr.status === 401) {
var user = getJwtSubject(jwt); var user = nf.Common.getJwtSubject(jwt);
// show the user // show the user
$('#nifi-user-submit-justification').text(user); $('#nifi-user-submit-justification').text(user);
@ -316,7 +295,7 @@ nf.Login = (function () {
if (xhr.status === 401) { if (xhr.status === 401) {
// attempt to get a token for the current user without passing login credentials // attempt to get a token for the current user without passing login credentials
token.done(function (jwt) { token.done(function (jwt) {
var user = getJwtSubject(jwt); var user = nf.Common.getJwtSubject(jwt);
// show the user // show the user
$('#nifi-user-submit-justification').text(user); $('#nifi-user-submit-justification').text(user);

View File

@ -74,10 +74,19 @@ $(document).ready(function () {
nf.Storage.removeItem('jwt'); nf.Storage.removeItem('jwt');
window.location = '/nifi/login'; window.location = '/nifi/login';
}); });
// schedule token refresh when a token is present
if (nf.Storage.getItem('jwt') !== null) {
nf.Common.scheduleTokenRefresh();
}
}); });
// Define a common utility class used across the entire application. // Define a common utility class used across the entire application.
nf.Common = { nf.Common = (function () {
// interval for cancelling token refresh when necessary
var tokenRefreshInterval = null;
return {
config: { config: {
sensitiveText: 'Sensitive value set', sensitiveText: 'Sensitive value set',
tooltipConfig: { tooltipConfig: {
@ -95,6 +104,9 @@ nf.Common = {
at: 'top right', at: 'top right',
my: 'bottom left' my: 'bottom left'
} }
},
urls: {
token: '../nifi-api/token'
} }
}, },
@ -130,6 +142,106 @@ nf.Common = {
}); });
}, },
/**
* Automatically refresh tokens by checking once an hour if its going to expire soon.
*/
scheduleTokenRefresh: function () {
// if we are currently polling for token refresh, cancel it
if (tokenRefreshInterval !== null) {
clearInterval(tokenRefreshInterval);
}
// set the interval to one hour
var interval = nf.Common.MILLIS_PER_HOUR;
var checkExpiration = function () {
var expiration = nf.Storage.getItemExpiration('jwt');
// ensure there is an expiration and token present
if (expiration !== null) {
var expirationDate = new Date(expiration);
var now = new Date();
// get the time remainging plus a little bonus time to reload the token
var timeRemaining = expirationDate.valueOf() - now.valueOf() - nf.Common.MILLIS_PER_MINUTE;
if (timeRemaining < interval) {
// if the token will expire before the next interval minus some bonus time, refresh now
$.ajax({
type: 'GET',
url: nf.Common.config.urls.token
}).done(function (jwt) {
nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt));
});
}
}
};
// perform initial check
checkExpiration();
// schedule subsequent checks
tokenRefreshInterval = setInterval(checkExpiration, interval);
},
/**
* Extracts the subject from the specified jwt. If the jwt is not as expected
* an empty string is returned.
*
* @param {string} jwt
* @returns {string}
*/
getJwtSubject: function (jwt) {
if (nf.Common.isDefinedAndNotNull(jwt)) {
var segments = jwt.split(/\./);
if (segments.length !== 3) {
return '';
}
var rawPayload = $.base64.atob(segments[1]);
var payload = JSON.parse(rawPayload);
if (nf.Common.isDefinedAndNotNull(payload['preferred_username'])) {
return payload['preferred_username'];
} else {
'';
}
}
return '';
},
/**
* Extracts the expiration from the specified jwt. If the jwt is not as expected
* a null value is returned.
*
* @param {string} jwt
* @returns {integer}
*/
getJwtExpiration: function (jwt) {
if (nf.Common.isDefinedAndNotNull(jwt)) {
var segments = jwt.split(/\./);
if (segments.length !== 3) {
return null;
}
var rawPayload = $.base64.atob(segments[1]);
var payload = JSON.parse(rawPayload);
if (nf.Common.isDefinedAndNotNull(payload['exp'])) {
try {
// jwt exp is in seconds
return parseInt(payload['exp'], 10) * nf.Common.MILLIS_PER_SECOND;
} catch (e) {
return null;
}
} else {
return null;
}
}
return null;
},
/** /**
* Determines whether the current user can access provenance. * Determines whether the current user can access provenance.
* *
@ -205,16 +317,6 @@ nf.Common = {
* @argument {string} error The error * @argument {string} error The error
*/ */
handleAjaxError: function (xhr, status, error) { handleAjaxError: function (xhr, status, error) {
// show the account registration page if necessary
// if (xhr.status === 401 && $('#registration-pane').length) {
// // show the registration pane
// $('#registration-pane').show();
//
// // close the canvas
// nf.Common.closeCanvas();
// return;
// }
// if an error occurs while the splash screen is visible close the canvas show the error message // if an error occurs while the splash screen is visible close the canvas show the error message
if ($('#splash').is(':visible')) { if ($('#splash').is(':visible')) {
if (xhr.status === 401) { if (xhr.status === 401) {
@ -1022,4 +1124,5 @@ nf.Common = {
}); });
return formattedBulletins; return formattedBulletins;
} }
}; };
}());

View File

@ -20,7 +20,61 @@
nf.Storage = (function () { nf.Storage = (function () {
// Store items for two days before being eligible for removal. // Store items for two days before being eligible for removal.
var TWO_DAYS = 86400000 * 2; var TWO_DAYS = nf.Common.MILLIS_PER_DAY * 2;
/**
* Checks the expiration for the specified entry.
*
* @param {object} entry
* @returns {boolean}
*/
var checkExpiration = function (entry) {
if (nf.Common.isDefinedAndNotNull(entry.expires)) {
// get the expiration
var expires = new Date(entry.expires);
var now = new Date();
// return whether the expiration date has passed
return expires.valueOf() < now.valueOf();
} else {
return false;
}
};
/**
* If the item at key is not expired, the value of field is returned. Otherwise, null.
*
* @param {string} key
* @param {string} field
* @return {object} the value
*/
var getEntryField = function (key, field) {
try {
// parse the entry
var entry = JSON.parse(localStorage.getItem(key));
// ensure the entry and item are present
if (nf.Common.isDefinedAndNotNull(entry)) {
// if the entry is expired, drop it and return null
if (checkExpiration(entry)) {
nf.Storage.removeItem(key);
return null;
}
// if the entry has the specified field return its value
if (nf.Common.isDefinedAndNotNull(entry[field])) {
return entry[field];
} else {
return null;
}
} else {
return null;
}
} catch (e) {
return null;
}
};
return { return {
/** /**
@ -34,16 +88,10 @@ nf.Storage = (function () {
var key = localStorage.key(i); var key = localStorage.key(i);
var entry = JSON.parse(localStorage.getItem(key)); var entry = JSON.parse(localStorage.getItem(key));
// get the expiration if (checkExpiration(entry)) {
var expires = new Date(entry.expires); nf.Storage.removeItem(key);
var now = new Date();
// if the expiration date has passed, remove it
if (expires.valueOf() < now.valueOf()) {
localStorage.removeItem(key);
} }
} catch (e) { } catch (e) {
// likely unable to parse the item
} }
} }
}, },
@ -51,12 +99,13 @@ nf.Storage = (function () {
/** /**
* Stores the specified item. * Stores the specified item.
* *
* @param {type} key * @param {string} key
* @param {type} item * @param {object} item
* @param {integer} expires
*/ */
setItem: function (key, item) { setItem: function (key, item, expires) {
// calculate the expiration // calculate the expiration
var expires = new Date().valueOf() + TWO_DAYS; expires = nf.Common.isDefinedAndNotNull(expires) ? expires : new Date().valueOf() + TWO_DAYS;
// create the entry // create the entry
var entry = { var entry = {
@ -76,19 +125,18 @@ nf.Storage = (function () {
* @param {type} key * @param {type} key
*/ */
getItem: function (key) { getItem: function (key) {
try { return getEntryField(key, 'item');
// parse the entry },
var entry = JSON.parse(localStorage.getItem(key));
// ensure the entry and item are present /**
if (nf.Common.isDefinedAndNotNull(entry) && nf.Common.isDefinedAndNotNull(entry.item)) { * Gets the expiration for the specified item. If the item does not exists our could
return entry.item; * not be parsed, returns null.
} else { *
return null; * @param {string} key
} * @returns {integer}
} catch (e) { */
return null; getItemExpiration: function (key) {
} return getEntryField(key, 'expires');
}, },
/** /**