|
9 | 9 | //var state = Date.now() + "" + Math.random(); |
10 | 10 |
|
11 | 11 | (function() { |
| 12 | + function expired(token) { |
| 13 | + return (token && token.expires_at && new Date(token.expires_at) < new Date()); |
| 14 | + }; |
12 | 15 | function getSessionToken($window) { |
13 | 16 | var tokenString = $window.sessionStorage.getItem('token'); |
14 | 17 | var token = null; |
15 | | - if (tokenString) { |
| 18 | + if (tokenString && tokenString !== "null" ) { |
16 | 19 | token = JSON.parse(tokenString); |
| 20 | + token.expires_at= new Date(token.expires_at); |
17 | 21 | } |
18 | 22 | return token; |
19 | 23 | } |
|
64 | 68 | service.get = function() { |
65 | 69 | return this.token; |
66 | 70 | }; |
67 | | - service.set = function() { |
| 71 | + service.set = function(trustedTokenHash) { |
68 | 72 | // Get and scrub the session stored state |
69 | 73 | var parsedFromHash = false; |
70 | 74 | var previousState = $window.sessionStorage.getItem('verifyState'); |
71 | 75 | $window.sessionStorage.setItem('verifyState', null); |
72 | 76 |
|
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) { |
74 | 84 | if ($location.path().length > 1) { |
75 | 85 | var values = $location.path().substring(1); |
76 | 86 | service.token = setTokenFromHashParams(values); |
|
125 | 135 | }; |
126 | 136 | service.destroy = function() { |
127 | 137 | $window.sessionStorage.setItem('token', null); |
128 | | - service.token = null; |
| 138 | + service.token = null; |
129 | 139 | }; |
130 | 140 |
|
131 | 141 | return service; |
132 | 142 | }]); |
133 | 143 |
|
134 | 144 | // Auth interceptor - if token is missing or has expired this broadcasts an authRequired event |
135 | 145 | 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 | | - }; |
139 | 146 |
|
140 | 147 | var service = { |
141 | 148 | request: function(config) { |
|
182 | 189 | }]); |
183 | 190 |
|
184 | 191 | // 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) { |
186 | 193 | var service = { |
187 | 194 | authorize: function() { |
| 195 | + accessToken.destroy(); |
188 | 196 | $window.sessionStorage.setItem('verifyState', service.state); |
189 | | - window.location.replace(service.url); |
| 197 | + window.location.replace(getAuthorizationUrl()); |
190 | 198 | }, |
191 | 199 | appendSignoutToken: false |
192 | 200 | }; |
193 | 201 |
|
| 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 | + |
194 | 265 | service.signOut = function(token) { |
195 | 266 | if (service.signOutUrl && service.signOutUrl.length > 0) { |
196 | 267 | var url = service.signOutUrl; |
|
206 | 277 | }; |
207 | 278 |
|
208 | 279 | 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(); |
216 | 287 | } |
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; |
218 | 294 | service.signOutUrl = params.signOutUrl; |
| 295 | + service.silentTokenRedirectUrl= params.silentTokenRedirectUrl; |
219 | 296 | service.signOutRedirectUrl = params.signOutRedirectUrl; |
220 | | - service.state = params.state; |
| 297 | + service.state = params.state || generateState(); |
221 | 298 | if (params.signOutAppendToken == 'true') { |
222 | 299 | service.appendSignoutToken = true; |
223 | 300 | } |
|
227 | 304 | }]); |
228 | 305 |
|
229 | 306 | // 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: { |
235 | 319 | authorizationUrl: '@', // authorization server url |
236 | 320 | clientId: '@', // client ID |
237 | 321 | redirectUrl: '@', // uri th auth server should redirect to (cannot contain #) |
|
245 | 329 | 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 |
246 | 330 | signOutAppendToken: '@', // defaults to 'false', set to 'true' to append the token to the sign out url |
247 | 331 | 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 |
248 | 333 | nonce: '@?', // nonce value, optional. If unspecified or an empty string and autoGenerateNonce is true then a nonce will be auto-generated |
249 | 334 | autoGenerateNonce: '=?' // Should a nonce be autogenerated if not supplied. Optional and defaults to true. |
250 | | - } |
251 | | - }; |
| 335 | + } |
| 336 | + }; |
252 | 337 |
|
253 | 338 | definition.link = function(scope, element, attrs) { |
254 | 339 | function compile() { |
|
266 | 351 |
|
267 | 352 | function routeChangeHandler(event, nextRoute) { |
268 | 353 | if (nextRoute.$$route && nextRoute.$$route.requireToken) { |
269 | | - if (!accessToken.get()) { |
| 354 | + if (!accessToken.get() || expired(accessToken.get())) { |
270 | 355 | event.preventDefault(); |
271 | 356 | $window.sessionStorage.setItem('oauthRedirectRoute', $location.path()); |
272 | 357 | endpoint.authorize(); |
273 | 358 | } |
274 | 359 | } |
275 | 360 | }; |
276 | 361 |
|
277 | | - function generateState() { |
278 | | - var text = ((Date.now() + Math.random()) * Math.random()).toString().replace(".",""); |
279 | | - return md5.createHash(text); |
280 | | - } |
281 | 362 |
|
282 | 363 | function init() { |
283 | 364 | scope.buttonClass = scope.buttonClass || 'btn btn-primary'; |
|
287 | 368 | scope.signOutUrl = scope.signOutUrl || ''; |
288 | 369 | scope.signOutRedirectUrl = scope.signOutRedirectUrl || ''; |
289 | 370 | scope.unauthorizedAccessUrl = scope.unauthorizedAccessUrl || ''; |
290 | | - scope.state = scope.state || generateState(); |
| 371 | + scope.silentTokenRedirectUrl = scope.silentTokenRedirectUrl || ''; |
291 | 372 | if (scope.autoGenerateNonce === undefined) { |
292 | 373 | scope.autoGenerateNonce = true; |
293 | 374 | } |
294 | | - if (!scope.nonce && scope.autoGenerateNonce) { |
295 | | - scope.nonce = generateState(); |
296 | | - } |
297 | | - |
298 | 375 | compile(); |
299 | 376 |
|
300 | 377 | endpoint.init(scope); |
301 | | - scope.signedIn = accessToken.set() !== null; |
302 | 378 | scope.$on('oauth2:authRequired', function() { |
303 | 379 | endpoint.authorize(); |
304 | 380 | }); |
| 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 | + }); |
305 | 397 | 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 | + } |
308 | 406 | } |
309 | 407 | }); |
310 | 408 | scope.$on('oauth2:authExpired', function() { |
311 | 409 | scope.signedIn = false; |
| 410 | + accessToken.destroy(); |
312 | 411 | }); |
| 412 | + scope.signedIn = accessToken.set() !== null; |
313 | 413 | $rootScope.$on('$routeChangeStart', routeChangeHandler); |
314 | 414 | } |
315 | 415 |
|
|
0 commit comments