Skip to content
This repository was archived by the owner on Jan 22, 2019. It is now read-only.

Commit 0a48d74

Browse files
committed
Merge pull request JamesRandall#21 from ciaranj/silent_token_renewal
Silent token renewal - thanks to ciaranj
2 parents 963ecee + ff3e8e6 commit 0a48d74

File tree

1 file changed

+138
-38
lines changed

1 file changed

+138
-38
lines changed

dist/angularJsOAuth2.js

Lines changed: 138 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@
99
//var state = Date.now() + "" + Math.random();
1010

1111
(function() {
12+
function expired(token) {
13+
return (token && token.expires_at && new Date(token.expires_at) < new Date());
14+
};
1215
function getSessionToken($window) {
1316
var tokenString = $window.sessionStorage.getItem('token');
1417
var token = null;
15-
if (tokenString) {
18+
if (tokenString && tokenString !== "null" ) {
1619
token = JSON.parse(tokenString);
20+
token.expires_at= new Date(token.expires_at);
1721
}
1822
return token;
1923
}
@@ -64,13 +68,19 @@
6468
service.get = function() {
6569
return this.token;
6670
};
67-
service.set = function() {
71+
service.set = function(trustedTokenHash) {
6872
// Get and scrub the session stored state
6973
var parsedFromHash = false;
7074
var previousState = $window.sessionStorage.getItem('verifyState');
7175
$window.sessionStorage.setItem('verifyState', null);
7276

73-
if ($location.$$html5) {
77+
if(trustedTokenHash) {
78+
// We 'trust' this hash as it was already 'parsed' by the child iframe before we got it as the parent
79+
// and then handed it back (not just reverifying as the sessionStorage was blanked by the child frame, so
80+
// we can't :(
81+
service.token = setTokenFromHashParams(trustedTokenHash);
82+
}
83+
else if ($location.$$html5) {
7484
if ($location.path().length > 1) {
7585
var values = $location.path().substring(1);
7686
service.token = setTokenFromHashParams(values);
@@ -125,17 +135,14 @@
125135
};
126136
service.destroy = function() {
127137
$window.sessionStorage.setItem('token', null);
128-
service.token = null;
138+
service.token = null;
129139
};
130140

131141
return service;
132142
}]);
133143

134144
// Auth interceptor - if token is missing or has expired this broadcasts an authRequired event
135145
angular.module('oauth2.interceptor', []).factory('OAuth2Interceptor', ['$rootScope', '$q', '$window', function ($rootScope, $q, $window) {
136-
var expired = function(token) {
137-
return (token && token.expires_at && new Date(token.expires_at) < new Date());
138-
};
139146

140147
var service = {
141148
request: function(config) {
@@ -182,15 +189,79 @@
182189
}]);
183190

184191
// Endpoint wrapper
185-
angular.module('oauth2.endpoint', []).factory('Endpoint', ['AccessToken', '$window', function(accessToken, $window) {
192+
angular.module('oauth2.endpoint', ['angular-md5']).factory('Endpoint', ['AccessToken', '$window', 'md5', '$rootScope', function(accessToken, $window, md5, $rootScope) {
186193
var service = {
187194
authorize: function() {
195+
accessToken.destroy();
188196
$window.sessionStorage.setItem('verifyState', service.state);
189-
window.location.replace(service.url);
197+
window.location.replace(getAuthorizationUrl());
190198
},
191199
appendSignoutToken: false
192200
};
193201

202+
function getAuthorizationUrl(performSilently) {
203+
var url= service.authorizationUrl + '?' +
204+
'client_id=' + encodeURIComponent(service.clientId) + '&' +
205+
'redirect_uri=' + encodeURIComponent(performSilently?service.silentTokenRedirectUrl:service.redirectUrl) + '&' +
206+
'response_type=' + encodeURIComponent(service.responseType) + '&' +
207+
'scope=' + encodeURIComponent(service.scope);
208+
if (service.nonce) {
209+
url += '&nonce=' + encodeURIComponent(service.nonce);
210+
}
211+
url += '&state=' + encodeURIComponent(service.state);
212+
213+
if( performSilently ) {
214+
url = url + "&prompt=none";
215+
}
216+
return url;
217+
}
218+
219+
service.renewTokenSilently= function() {
220+
function setupTokenSilentRenewInTheFuture() {
221+
var frame= $window.document.createElement("iframe");
222+
frame.style.display = "none";
223+
$window.sessionStorage.setItem('verifyState', service.state);
224+
frame.src= getAuthorizationUrl(true);
225+
function cleanup() {
226+
$window.removeEventListener("message", message, false);
227+
if( handle) {
228+
window.clearTimeout(handle);
229+
}
230+
handle= null;
231+
$window.setTimeout(function() {
232+
// Complete this on another tick of the eventloop to allow angular (in the child frame) to complete nicely.
233+
$window.document.body.removeChild(frame);
234+
}, 0);
235+
}
236+
237+
function message(e) {
238+
if (handle && e.origin === location.protocol + "//" + location.host && e.source == frame.contentWindow) {
239+
cleanup();
240+
if( e.data === "oauth2.silentRenewFailure" ) {
241+
$rootScope.$broadcast('oauth2:authExpired');
242+
}
243+
else {
244+
accessToken.set(e.data);
245+
}
246+
}
247+
}
248+
249+
var handle= window.setTimeout(function() {
250+
cleanup();
251+
}, 5000);
252+
$window.addEventListener("message", message, false);
253+
$window.document.body.appendChild(frame);
254+
};
255+
256+
var now= new Date();
257+
// Renew the token 1 minute before we expect it to expire. N.B. This code elsewhere sets the expires_at to be 60s less than the server-decided expiry time
258+
// this has the effect of reducing access token lifetimes by a mininum of 2 minutes, and restricts you to producing access tokens that are at *least* this long lived
259+
260+
var renewTokenAt= new Date( accessToken.get().expires_at.getTime() - 60000 );
261+
var renewTokenIn= renewTokenAt - new Date();
262+
window.setTimeout(setupTokenSilentRenewInTheFuture, renewTokenIn);
263+
};
264+
194265
service.signOut = function(token) {
195266
if (service.signOutUrl && service.signOutUrl.length > 0) {
196267
var url = service.signOutUrl;
@@ -206,18 +277,24 @@
206277
};
207278

208279
service.init = function(params) {
209-
service.url = params.authorizationUrl + '?' +
210-
'client_id=' + encodeURIComponent(params.clientId) + '&' +
211-
'redirect_uri=' + encodeURIComponent(params.redirectUrl) + '&' +
212-
'response_type=' + encodeURIComponent(params.responseType) + '&' +
213-
'scope=' + encodeURIComponent(params.scope) + '&';
214-
if (params.nonce) {
215-
service.url += 'nonce=' + encodeURIComponent(params.nonce) + '&';
280+
function generateState() {
281+
var text = ((Date.now() + Math.random()) * Math.random()).toString().replace(".","");
282+
return md5.createHash(text);
283+
}
284+
285+
if (!params.nonce && params.autoGenerateNonce) {
286+
params.nonce = generateState();
216287
}
217-
service.url += 'state=' + encodeURIComponent(params.state);
288+
service.nonce = params.nonce;
289+
service.clientId= params.clientId;
290+
service.redirectUrl= params.redirectUrl;
291+
service.scope= params.scope;
292+
service.responseType= params.responseType;
293+
service.authorizationUrl= params.authorizationUrl;
218294
service.signOutUrl = params.signOutUrl;
295+
service.silentTokenRedirectUrl= params.silentTokenRedirectUrl;
219296
service.signOutRedirectUrl = params.signOutRedirectUrl;
220-
service.state = params.state;
297+
service.state = params.state || generateState();
221298
if (params.signOutAppendToken == 'true') {
222299
service.appendSignoutToken = true;
223300
}
@@ -227,11 +304,18 @@
227304
}]);
228305

229306
// Open ID directive
230-
angular.module('oauth2.directive', ['angular-md5']).directive('oauth2', ['$rootScope', '$http', '$window', '$location', '$templateCache', '$compile', 'AccessToken', 'Endpoint', 'md5', function($rootScope, $http, $window, $location, $templateCache, $compile, accessToken, endpoint, md5) {
231-
var definition = {
232-
restrict: 'E',
233-
replace: true,
234-
scope: {
307+
angular.module('oauth2.directive', [])
308+
.config(['$routeProvider', function ($routeProvider) {
309+
$routeProvider
310+
.when('/silent-renew', {
311+
template: ""
312+
})
313+
}])
314+
.directive('oauth2', ['$rootScope', '$http', '$window', '$location', '$templateCache', '$compile', 'AccessToken', 'Endpoint', function($rootScope, $http, $window, $location, $templateCache, $compile, accessToken, endpoint) {
315+
var definition = {
316+
restrict: 'E',
317+
replace: true,
318+
scope: {
235319
authorizationUrl: '@', // authorization server url
236320
clientId: '@', // client ID
237321
redirectUrl: '@', // uri th auth server should redirect to (cannot contain #)
@@ -245,10 +329,11 @@
245329
signOutUrl: '@', // url on the authorization server for logging out. Local token is deleted even if no URL is given but that will leave user logged in against STS
246330
signOutAppendToken: '@', // defaults to 'false', set to 'true' to append the token to the sign out url
247331
signOutRedirectUrl: '@', // url to redirect to after sign out on the STS has completed
332+
silentTokenRedirectUrl: '@', // url to use for silently renewing access tokens, default behaviour is not to do
248333
nonce: '@?', // nonce value, optional. If unspecified or an empty string and autoGenerateNonce is true then a nonce will be auto-generated
249334
autoGenerateNonce: '=?' // Should a nonce be autogenerated if not supplied. Optional and defaults to true.
250-
}
251-
};
335+
}
336+
};
252337

253338
definition.link = function(scope, element, attrs) {
254339
function compile() {
@@ -266,18 +351,14 @@
266351

267352
function routeChangeHandler(event, nextRoute) {
268353
if (nextRoute.$$route && nextRoute.$$route.requireToken) {
269-
if (!accessToken.get()) {
354+
if (!accessToken.get() || expired(accessToken.get())) {
270355
event.preventDefault();
271356
$window.sessionStorage.setItem('oauthRedirectRoute', $location.path());
272357
endpoint.authorize();
273358
}
274359
}
275360
};
276361

277-
function generateState() {
278-
var text = ((Date.now() + Math.random()) * Math.random()).toString().replace(".","");
279-
return md5.createHash(text);
280-
}
281362

282363
function init() {
283364
scope.buttonClass = scope.buttonClass || 'btn btn-primary';
@@ -287,29 +368,48 @@
287368
scope.signOutUrl = scope.signOutUrl || '';
288369
scope.signOutRedirectUrl = scope.signOutRedirectUrl || '';
289370
scope.unauthorizedAccessUrl = scope.unauthorizedAccessUrl || '';
290-
scope.state = scope.state || generateState();
371+
scope.silentTokenRedirectUrl = scope.silentTokenRedirectUrl || '';
291372
if (scope.autoGenerateNonce === undefined) {
292373
scope.autoGenerateNonce = true;
293374
}
294-
if (!scope.nonce && scope.autoGenerateNonce) {
295-
scope.nonce = generateState();
296-
}
297-
298375
compile();
299376

300377
endpoint.init(scope);
301-
scope.signedIn = accessToken.set() !== null;
302378
scope.$on('oauth2:authRequired', function() {
303379
endpoint.authorize();
304380
});
381+
scope.$on('oauth2:authSuccess', function() {
382+
if (scope.silentTokenRedirectUrl.length > 0) {
383+
if( $location.path().indexOf("/silent-renew") == 0 ) {
384+
// A 'child' frame has successfully authorised an access token.
385+
if (window.top && window.parent && window !== window.top) {
386+
var hash = hash || window.location.hash;
387+
if (hash) {
388+
window.parent.postMessage(hash, location.protocol + "//" + location.host);
389+
}
390+
}
391+
} else {
392+
// An 'owning' frame has successfully authorised an access token.
393+
endpoint.renewTokenSilently();
394+
}
395+
}
396+
});
305397
scope.$on('oauth2:authError', function() {
306-
if (scope.unauthorizedAccessUrl.length > 0) {
307-
$location.path(scope.unauthorizedAccessUrl);
398+
if( $location.path().indexOf("/silent-renew") == 0 && window.top && window.parent && window !== window.top) {
399+
// A 'child' frame failed to authorize.
400+
window.parent.postMessage("oauth2.silentRenewFailure", location.protocol + "//" + location.host);
401+
}
402+
else {
403+
if (scope.unauthorizedAccessUrl.length > 0) {
404+
$location.path(scope.unauthorizedAccessUrl);
405+
}
308406
}
309407
});
310408
scope.$on('oauth2:authExpired', function() {
311409
scope.signedIn = false;
410+
accessToken.destroy();
312411
});
412+
scope.signedIn = accessToken.set() !== null;
313413
$rootScope.$on('$routeChangeStart', routeChangeHandler);
314414
}
315415

0 commit comments

Comments
 (0)