Access tokens are now refreshed in preflight promises.
This patch modifies the access token header to return a promise, which will resolve only if it has performed a best-effort attempt to provide a valid OAuth token. This provides several benefits: - It removes the need for a timer inside the refresh manager. In fact, the entire refresh manager is now obsolete. - No HTTP request is sent to the API without a token. - Significant reduction of 401 errors being raised to the user. - Validating a token now simply requires a single HTTP request, after which the AccessToken provider can be assumed to be current. - Makes 401 error handling mostly irrelevant. The remaining edge case is when a refresh token expires. Change-Id: I52168485c8236f93a85d3d2b6033a01293e7b747
This commit is contained in:
parent
a3abee9058
commit
61d33605c4
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An HTTP request interceptor that attaches an authorization to every HTTP
|
||||
* request, assuming it exists and isn't expired.
|
||||
*/
|
||||
angular.module('sb.auth').factory('httpAuthorizationHeader',
|
||||
function (AccessToken) {
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
request: function (request) {
|
||||
|
||||
// TODO(krotscheck): Only apply the token to requests to
|
||||
// storyboardApiBase.
|
||||
var token = AccessToken.getAccessToken();
|
||||
var type = AccessToken.getTokenType();
|
||||
if (!!token) {
|
||||
request.headers.Authorization = type + ' ' + token;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
};
|
||||
})
|
||||
// Attach the HTTP interceptor.
|
||||
.config(function ($httpProvider) {
|
||||
'use strict';
|
||||
|
||||
$httpProvider.interceptors.push('httpAuthorizationHeader');
|
||||
});
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright (c) 2016 Hewlett Packard Enterprise Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This $http interceptor ensures that the OAuth Token is always valid, fresh,
|
||||
* and reissued when needed.
|
||||
*/
|
||||
angular.module('sb.auth').factory('httpOAuthTokenInterceptor',
|
||||
function (AccessToken, $injector, $q, $log) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Promise deferral for when we're in-flight of a refresh token request.
|
||||
*
|
||||
* @type {Promise}
|
||||
*/
|
||||
var refreshPromise = null;
|
||||
|
||||
/**
|
||||
* Returns the current access token, if available.
|
||||
* @returns {*} The access token.
|
||||
*/
|
||||
function getCurrentToken() {
|
||||
return $q.when({
|
||||
type: AccessToken.getTokenType() || null,
|
||||
value: AccessToken.getAccessToken() || null,
|
||||
expired: AccessToken.isExpired() || AccessToken.expiresSoon(),
|
||||
refresh: AccessToken.getRefreshToken() || null
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks a token (see above) to see whether it needs to
|
||||
* be refreshed. If yes, it will refresh that token, and return a
|
||||
* promise that will update the token in the promise chain.
|
||||
* Otherwise, it will simply pass the token along to the header
|
||||
* decorator.
|
||||
*
|
||||
* @param {{}} token The token to check.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function refreshIfNeeded(token) {
|
||||
// If it's not expired, just pass it on.
|
||||
if (!token.expired) {
|
||||
return $q.when(token);
|
||||
}
|
||||
|
||||
// If we don't have a refresh token, pass it on.
|
||||
if (!token.refresh) {
|
||||
return $q.when(token);
|
||||
}
|
||||
|
||||
// If the refresh promise is already in flight, return that. We
|
||||
// don't want to get caught in a situation where we try to refresh
|
||||
// more than once.
|
||||
if (refreshPromise) {
|
||||
$log.debug('Returning in-flight refresh promise.');
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
// We have to refresh.
|
||||
$log.debug('Attempting to refresh auth token.');
|
||||
var deferred = $q.defer();
|
||||
refreshPromise = deferred.promise;
|
||||
|
||||
// Issue a refresh token request. We have to manually grab the
|
||||
// service from the injector here, because else we'd have a
|
||||
// circular injection dependency.
|
||||
var OpenId = $injector.get('OpenId');
|
||||
OpenId.token({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: token.refresh
|
||||
}).then(
|
||||
function (data) {
|
||||
AccessToken.setToken(data);
|
||||
},
|
||||
function () {
|
||||
AccessToken.clear();
|
||||
}
|
||||
).finally(function () {
|
||||
// Inject the token, whether or not it exists, back into the
|
||||
// promise chain.
|
||||
deferred.resolve(getCurrentToken());
|
||||
|
||||
// Clear the promise;
|
||||
refreshPromise = null;
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* The request interceptor ensures that the OAuth token, if
|
||||
* available, is attached to the HTTP request.
|
||||
*
|
||||
* @param request The request.
|
||||
* @returns {*} A promise that will resolve once we're sure we
|
||||
* have a good token.
|
||||
*/
|
||||
request: function (httpConfig) {
|
||||
/**
|
||||
* Decorate the request with the header token, IFF it's
|
||||
* available.
|
||||
*
|
||||
* @param token The token received from the promise chain.
|
||||
* @returns {Promise} A promise that resolves the httpconfig
|
||||
*/
|
||||
function decorateHeader(token) {
|
||||
if (token.type && token.value) {
|
||||
httpConfig.headers.Authorization =
|
||||
token.type + ' ' + token.value;
|
||||
}
|
||||
return $q.when(httpConfig);
|
||||
}
|
||||
|
||||
// Are we an OpenID request? These get skipped.
|
||||
if(httpConfig.url.indexOf('/openid/') > -1) {
|
||||
return $q.when(httpConfig);
|
||||
}
|
||||
|
||||
// Build the interceptor promise chain for each request.
|
||||
return $q.when(getCurrentToken())
|
||||
.then(refreshIfNeeded)
|
||||
.then(decorateHeader);
|
||||
}
|
||||
};
|
||||
})
|
||||
// Attach the HTTP interceptor.
|
||||
.config(function ($httpProvider) {
|
||||
'use strict';
|
||||
|
||||
$httpProvider.interceptors.unshift('httpOAuthTokenInterceptor');
|
||||
});
|
|
@ -60,7 +60,7 @@ angular.module('sb.auth', [ 'sb.services', 'sb.templates', 'ui.router',
|
|||
});
|
||||
})
|
||||
.run(function ($rootScope, SessionState, Session, PermissionManager,
|
||||
RefreshManager, Notification, Priority) {
|
||||
Notification, Priority) {
|
||||
'use strict';
|
||||
|
||||
// Initialize our permission manager.
|
||||
|
@ -79,6 +79,4 @@ angular.module('sb.auth', [ 'sb.services', 'sb.templates', 'ui.router',
|
|||
break;
|
||||
}
|
||||
}, Priority.LAST);
|
||||
|
||||
RefreshManager.scheduleRefresh();
|
||||
});
|
||||
|
|
|
@ -15,18 +15,9 @@
|
|||
*/
|
||||
|
||||
angular.module('sb.auth').run(
|
||||
function($log, $modal, Notification, RefreshManager, Session, Priority) {
|
||||
function($log, $modal, Notification, Session, Priority) {
|
||||
'use strict';
|
||||
|
||||
function handle_401() {
|
||||
RefreshManager.tryRefresh().then(function () {
|
||||
$log.info('Token refreshed on 401');
|
||||
}, function () {
|
||||
$log.info('Could not refresh token. Destroying session');
|
||||
Session.destroySession();
|
||||
});
|
||||
}
|
||||
|
||||
function handle_403() {
|
||||
var modalInstance = $modal.open({
|
||||
templateUrl: 'app/auth/template/modal/superuser_required.html',
|
||||
|
@ -44,12 +35,6 @@ angular.module('sb.auth').run(
|
|||
// intercepted before anything else happens.
|
||||
Notification.intercept(function (message) {
|
||||
if (message.type === 'http') {
|
||||
if (message.message === 401) {
|
||||
// An unauthorized error. Refreshing the access token
|
||||
// might help.
|
||||
handle_401();
|
||||
}
|
||||
|
||||
if (message.message === 403) {
|
||||
// Forbidden error. A user should be warned tha he is
|
||||
// doing something wrong.
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
angular.module('sb.auth').service('RefreshManager',
|
||||
function ($q, $log, $timeout, preExpireDelta, AccessToken, OpenId) {
|
||||
'use strict';
|
||||
|
||||
var currentRefresh = null;
|
||||
var nextRefreshPromise = null;
|
||||
var scheduledForToken = null;
|
||||
var self = this;
|
||||
|
||||
// Try to refresh the expired access_token
|
||||
this.tryRefresh = function () {
|
||||
|
||||
if (!currentRefresh) {
|
||||
// Create our promise, since we should always return one.
|
||||
currentRefresh = $q.defer();
|
||||
currentRefresh.promise.then(
|
||||
function () {
|
||||
currentRefresh = null;
|
||||
},
|
||||
function () {
|
||||
currentRefresh = null;
|
||||
}
|
||||
);
|
||||
|
||||
var refresh_token = AccessToken.getRefreshToken();
|
||||
var is_expired = AccessToken.isExpired();
|
||||
var expires_soon = AccessToken.expiresSoon();
|
||||
|
||||
// Do we have a refresh token to try?
|
||||
if (!refresh_token) {
|
||||
$log.info('No refresh token found. Aborting refresh.');
|
||||
currentRefresh.reject();
|
||||
} else if (!is_expired && !expires_soon) {
|
||||
$log.info('No refresh required for current access token.');
|
||||
currentRefresh.resolve(true);
|
||||
} else {
|
||||
|
||||
$log.info('Trying to refresh token');
|
||||
OpenId.token({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refresh_token
|
||||
}).then(
|
||||
function (data) {
|
||||
AccessToken.setToken(data);
|
||||
currentRefresh.resolve(true);
|
||||
self.scheduleRefresh();
|
||||
},
|
||||
function () {
|
||||
AccessToken.clear();
|
||||
currentRefresh.reject();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return currentRefresh.promise;
|
||||
};
|
||||
|
||||
|
||||
this.scheduleRefresh = function () {
|
||||
if (!AccessToken.getRefreshToken() || AccessToken.isExpired()) {
|
||||
$log.info('Current token does not require deferred refresh.');
|
||||
return;
|
||||
}
|
||||
|
||||
var expiresAt = AccessToken.getIssueDate() +
|
||||
AccessToken.getExpiresIn();
|
||||
|
||||
if (!!nextRefreshPromise &&
|
||||
AccessToken.getAccessToken() === scheduledForToken) {
|
||||
|
||||
$log.info('The refresh is already scheduled.');
|
||||
return;
|
||||
}
|
||||
|
||||
var now = Math.round((new Date()).getTime() / 1000);
|
||||
var delay = (expiresAt - preExpireDelta - now) * 1000;
|
||||
nextRefreshPromise = $timeout(self.tryRefresh, delay, false);
|
||||
scheduledForToken = AccessToken.getAccessToken();
|
||||
|
||||
$log.info('Refresh scheduled to happen in ' + delay + ' ms');
|
||||
};
|
||||
}
|
||||
);
|
|
@ -20,7 +20,7 @@
|
|||
*/
|
||||
angular.module('sb.auth').factory('Session',
|
||||
function (SessionState, AccessToken, $rootScope, $log, $q, $state,
|
||||
SystemInfo, RefreshManager, Notification, Severity) {
|
||||
SystemInfo, Notification, Severity) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
@ -53,15 +53,14 @@ angular.module('sb.auth').factory('Session',
|
|||
*/
|
||||
|
||||
var deferred = $q.defer();
|
||||
RefreshManager.tryRefresh().then(function () {
|
||||
SystemInfo.get({},
|
||||
function (info) {
|
||||
return SystemInfo.get({},
|
||||
function (info) {
|
||||
if (AccessToken.getAccessToken()) {
|
||||
deferred.resolve(info);
|
||||
}, function (error) {
|
||||
deferred.reject(error);
|
||||
});
|
||||
});
|
||||
return deferred.promise;
|
||||
} else {
|
||||
deferred.reject(info);
|
||||
}
|
||||
}).$promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue