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/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.base64.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/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/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/jquery.base64.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.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-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.base64.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/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-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.base64.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/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-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.base64.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/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-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.base64.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/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/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/jquery.base64.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/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-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.base64.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/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-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.base64.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/combo/jquery.combo.js?${project.version}"></script>

View File

@ -98,7 +98,7 @@ nf.Login = (function () {
}
}).done(function (jwt) {
// 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
$.ajax({
@ -109,15 +109,18 @@ nf.Login = (function () {
if (response.identity === 'anonymous') {
showLogoutLink();
// schedule automatic token refresh
nf.Common.scheduleTokenRefresh();
// show the user
var user = getJwtSubject(jwt);
var user = nf.Common.getJwtSubject(jwt);
$('#nifi-user-submit-justification').text(user);
// show the registration form
initializeNiFiRegistration();
showNiFiRegistration();
} else {
// reload as appropriate
// reload as appropriate - no need to schedule token refresh as the page is reloading
if (top !== window) {
parent.window.location = '/nifi';
} else {
@ -127,8 +130,11 @@ nf.Login = (function () {
}).fail(function (xhr, status, error) {
showLogoutLink();
// schedule automatic token refresh
nf.Common.scheduleTokenRefresh();
// show the user
var user = getJwtSubject(jwt);
var user = nf.Common.getJwtSubject(jwt);
$('#nifi-user-submit-justification').text(user);
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 () {
nf.Storage.removeItem('jwt');
};
@ -272,7 +251,7 @@ nf.Login = (function () {
$('#login-message').text('Your account is active and you are already logged in.');
}).fail(function (xhr, status, error) {
if (xhr.status === 401) {
var user = getJwtSubject(jwt);
var user = nf.Common.getJwtSubject(jwt);
// show the user
$('#nifi-user-submit-justification').text(user);
@ -316,7 +295,7 @@ nf.Login = (function () {
if (xhr.status === 401) {
// attempt to get a token for the current user without passing login credentials
token.done(function (jwt) {
var user = getJwtSubject(jwt);
var user = nf.Common.getJwtSubject(jwt);
// show the user
$('#nifi-user-submit-justification').text(user);

View File

@ -74,10 +74,19 @@ $(document).ready(function () {
nf.Storage.removeItem('jwt');
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.
nf.Common = {
nf.Common = (function () {
// interval for cancelling token refresh when necessary
var tokenRefreshInterval = null;
return {
config: {
sensitiveText: 'Sensitive value set',
tooltipConfig: {
@ -95,6 +104,9 @@ nf.Common = {
at: 'top right',
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.
*
@ -205,16 +317,6 @@ nf.Common = {
* @argument {string} error The 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 ($('#splash').is(':visible')) {
if (xhr.status === 401) {
@ -1022,4 +1124,5 @@ nf.Common = {
});
return formattedBulletins;
}
};
};
}());

View File

@ -20,7 +20,61 @@
nf.Storage = (function () {
// 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 {
/**
@ -34,16 +88,10 @@ nf.Storage = (function () {
var key = localStorage.key(i);
var entry = JSON.parse(localStorage.getItem(key));
// get the expiration
var expires = new Date(entry.expires);
var now = new Date();
// if the expiration date has passed, remove it
if (expires.valueOf() < now.valueOf()) {
localStorage.removeItem(key);
if (checkExpiration(entry)) {
nf.Storage.removeItem(key);
}
} catch (e) {
// likely unable to parse the item
}
}
},
@ -51,12 +99,13 @@ nf.Storage = (function () {
/**
* Stores the specified item.
*
* @param {type} key
* @param {type} item
* @param {string} key
* @param {object} item
* @param {integer} expires
*/
setItem: function (key, item) {
setItem: function (key, item, expires) {
// calculate the expiration
var expires = new Date().valueOf() + TWO_DAYS;
expires = nf.Common.isDefinedAndNotNull(expires) ? expires : new Date().valueOf() + TWO_DAYS;
// create the entry
var entry = {
@ -76,19 +125,18 @@ nf.Storage = (function () {
* @param {type} key
*/
getItem: function (key) {
try {
// parse the entry
var entry = JSON.parse(localStorage.getItem(key));
return getEntryField(key, 'item');
},
// ensure the entry and item are present
if (nf.Common.isDefinedAndNotNull(entry) && nf.Common.isDefinedAndNotNull(entry.item)) {
return entry.item;
} else {
return null;
}
} catch (e) {
return null;
}
/**
* Gets the expiration for the specified item. If the item does not exists our could
* not be parsed, returns null.
*
* @param {string} key
* @returns {integer}
*/
getItemExpiration: function (key) {
return getEntryField(key, 'expires');
},
/**