-
Notifications
You must be signed in to change notification settings - Fork 27
/
progress.auth.sso.js
495 lines (424 loc) · 22.4 KB
/
progress.auth.sso.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
/*
Copyright (c) 2016-2019 Progress Software Corporation and/or its subsidiaries or affiliates.
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.
*/
(function () {
"use strict"; // note that this makes JSLint complain if you use arguments[x]
/*global progress : true*/
var fn;
// ADD AN OPTIONS PARAM THAT CAN INCLUDE A NAME FOR PAGE REFRESH?
progress.data.AuthenticationProviderSSO = function (uri) {
var that = this,
// SSO specific
_automaticTokenRefresh,
temp,
ssoTokenInfo = null,
tokenDataKeys = { // SSO specific
token: ".access_token",
refreshToken: ".refresh_token",
tokenType: ".token_type",
expiration: ".expires_in",
accessTokenExpiration: ".accessTokenExpiration"
};
// PRIVATE FUNCTIONS
// (The constructor uses local variables and functions mainly to try to protect the token
// information as much as possible. A few could probably be defined as properties/methods, but
// there's currently no need for that because the AuthenticationProvider API has no objects
// that inherit from AuthenticationProviderSSO.)
// Store the given token with the uri as the key. setItem() throws
// a "QuotaExceededError" error if there is insufficient storage space or
// "the user has disabled storage for the site" (Web storage spec at WHATWG)
function storeTokenInfo(info) {
var date,
accessTokenExpiration;
if (info.access_token.length) {
that._storage.setItem(tokenDataKeys.token, JSON.stringify(info.access_token));
}
if (info.refresh_token.length) {
that._storage.setItem(tokenDataKeys.refreshToken, JSON.stringify(info.refresh_token));
// The time given for the access token's expiration is in seconds. We transform it
// into milliseconds and add it to date.getTime() for a more standard format.
date = new Date();
// This should probably be renamed accessTokenRefreshThreshold
accessTokenExpiration = date.getTime() + (info.expires_in * 1000 * 0.75);
that._storage.setItem(tokenDataKeys.accessTokenExpiration, JSON.stringify(accessTokenExpiration));
} else {
// if there is no refresh token, remove any existing one. This handles the case where
// we got a new token via refresh, but now we're not being given any more refresh tokens
that._storage.removeItem(tokenDataKeys.refreshToken);
that._storage.removeItem(tokenDataKeys.accessTokenExpiration);
}
that._storage.setItem(tokenDataKeys.tokenType, JSON.stringify(info.token_type));
that._storage.setItem(tokenDataKeys.expiration, JSON.stringify(info.expires_in));
}
// get one of the pieces of data related to tokens from storage (could be the token itself, or
// the refresh token, expiration info, etc.). Returns null if the item isn't in storage
function retrieveTokenProperty(propName) {
var jsonStr = that._storage.getItem(propName),
value = null;
if (jsonStr !== null) {
try {
value = JSON.parse(jsonStr);
} catch (e) {
value = null;
}
}
return value;
}
function retrieveToken() {
return retrieveTokenProperty(tokenDataKeys.token);
}
function retrieveRefreshToken() {
return retrieveTokenProperty(tokenDataKeys.refreshToken);
}
function retrieveAccessTokenExpiration() {
return retrieveTokenProperty(tokenDataKeys.accessTokenExpiration);
}
function retrieveTokenType() {
return retrieveTokenProperty(tokenDataKeys.tokenType);
}
// This is going to be hardcoded for now. This can very
// possibly change in the future if we decide to expose
// the token to the user.
function getToken() {
return retrieveToken();
}
function retrieveExpiration() {
return retrieveTokenProperty(tokenDataKeys.expiration);
}
function clearTokenInfo(info) {
that._storage.removeItem(tokenDataKeys.token);
that._storage.removeItem(tokenDataKeys.refreshToken);
that._storage.removeItem(tokenDataKeys.tokenType);
that._storage.removeItem(tokenDataKeys.expiration);
that._storage.removeItem(tokenDataKeys.accessTokenExpiration);
}
// function is SSO specific
function openRefreshRequest(xhr) {
xhr.open('POST', that._refreshURI, true);
xhr.setRequestHeader("Cache-Control", "max-age=0");
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Accept", "application/json");
}
// function is SSO specific
function processRefreshResult(xhr, deferred) {
var errorObject,
result,
ssoTokenJSON;
if (xhr.status === 200) {
// get token and store it; if that goes well, resolve the promise, otherwise reject it
try {
ssoTokenInfo = JSON.parse(xhr.responseText);
if (ssoTokenInfo.access_token) {
storeTokenInfo(ssoTokenInfo);
// got the token info, its access_token has a value, and storeTokenInfo()
// didn't thrown an error, so call this a success
result = progress.data.Session.SUCCESS;
} else {
result = progress.data.Session.GENERAL_FAILURE;
// {1}: Unexpected error calling refresh: {error-string}
// ( No token returned from server)
errorObject = new Error(progress.data._getMsgText(
"jsdoMSG049",
"AuthenticationProvider",
"refresh",
progress.data._getMsgText("jsdoMSG050")
));
}
} catch (ex) {
result = progress.data.Session.GENERAL_FAILURE;
// {1}: Unexpected error calling refresh: {error-string}
// (error could be thrown from storeTokenInfo when it calls setItem())
errorObject = new Error(progress.data._getMsgText(
"jsdoMSG049",
"AuthenticationProvider",
"refresh",
ex.message
));
}
} else if (xhr.status === 401) {
that._reset(); // treat authentication failure as the equivalent of a logout
result = progress.data.Session.AUTHENTICATION_FAILURE;
} else {
result = progress.data.Session.GENERAL_FAILURE;
}
that._settlePromise(deferred, result, {"xhr": xhr,
"errorObject": errorObject}); // OK if undefined
}
this._processLoginResult = function (xhr, deferred) {
var errorObject,
result,
ssoTokenJSON;
if (xhr.status === 200) {
// Need to set loggedIn now so we can call logout from here if there's an
// error processing the response (e.g., authentication succeeded but we didn't get a
// token for some reason)
this._loggedIn = true;
// get token and store it; if that goes well, resolve the promise, otherwise reject it
try {
ssoTokenInfo = JSON.parse(xhr.responseText);
if (ssoTokenInfo.access_token) {
storeTokenInfo(ssoTokenInfo);
// got the token info, its access_token has a value, and storeTokenInfo()
// didn't throw an error, so call this a success
result = progress.data.Session.SUCCESS;
} else {
result = progress.data.Session.GENERAL_FAILURE;
// {1}: Unexpected error calling login: {error-string}
// ( No token returned from server)
errorObject = new Error(progress.data._getMsgText(
"jsdoMSG049",
"AuthenticationProvider",
"login",
progress.data._getMsgText("jsdoMSG050")
));
}
} catch (ex) {
result = progress.data.Session.GENERAL_FAILURE;
// {1}: Unexpected error calling login: {error-string}
// (error could be thrown from storeTokenInfo when it calls setItem())
errorObject = new Error(progress.data._getMsgText(
"jsdoMSG049",
"AuthenticationProvider",
"login",
ex.message
));
}
// log out if there was an error processing the response so the app can try to log in again
if (result !== progress.data.Session.SUCCESS) {
// call logout, but ignore its outcome -- just tell caller that login failed
var callback = function (params) {
params = progress.util.Deferred.getParamObject(params);
params.provider._settlePromise(deferred, result, {"xhr": xhr,
"errorObject": errorObject});
};
// finally
this.logout()
.then(callback, callback);
return; // so we don't execute the reject below, which could invoke the fail handler
// before we're done with the logout
}
} else if (xhr.status === 401) {
result = progress.data.Session.AUTHENTICATION_FAILURE;
} else {
result = progress.data.Session.GENERAL_FAILURE;
}
this._settlePromise(deferred, result, {"xhr": xhr});
};
// Override the protoype's method but call it from within the override. (Define the override
// here in the constructor so it has access to the internal function and variable)
this._reset = function () {
progress.data.AuthenticationProviderSSO.prototype._reset.apply(this);
clearTokenInfo();
ssoTokenInfo = null;
};
// Override the protoype's method but call it from within the override. (Define the override
// here in the constructor so it has access to the internal function getToken() )
// TODO: This method uses a callback, primarily to avoid breaking tdriver tests. We should change
// it to use promises
this._openRequestAndAuthorize = function (xhr,
verb,
uri,
async,
callback) {
var that = this,
date,
errorObject;
function afterRefreshCheck(provider, result, info) {
// if refresh failed because of auth failure, we will have gotten rid of the
// token and reset the auth provider
if (result === progress.data.Session.AUTHENTICATION_FAILURE) {
callback(new Error(progress.data._getMsgText("jsdoMSG060")));
} else {
// We've done the refresh check (and possible refresh) for SSO, now execute
// the base _openRequest... method, which does common things for Form-based
progress.data.AuthenticationProviderSSO.prototype._openRequestAndAuthorize.apply(
that,
[xhr, verb, uri, async, function (errorObject) {
if (errorObject instanceof Error) {
callback(errorObject);
} else {
xhr.setRequestHeader('Authorization', "oecp " + getToken());
callback(xhr);
}
}]
);
}
}
if (this.hasClientCredentials()) {
// Every token given has an expiration "hint". If the token's lifespan
// is close to or past that limit, then a refresh is done.
// No matter what the outcome of the refresh, keep in mind we always
// send the original request.
date = new Date();
if (this.automaticTokenRefresh &&
this.hasRefreshToken() &&
date.getTime() > retrieveAccessTokenExpiration()) {
try {
var callback = function (params, result, info) {
params = progress.util.Deferred.getParamObject(params, result, info);
afterRefreshCheck(params.provider, params.result, params.info);
};
// finally
this.refresh()
.then(callback, callback);
} catch (e) {
callback(e);
}
} else {
afterRefreshCheck(this, progress.data.Session.SUCCESS, null);
}
} else {
// This message is SSO specific, unless we can come up with a more general message
// JSDOSession: The AuthenticationProvider needs to be managing a valid token.
errorObject = new Error(progress.data._getMsgText("jsdoMSG125", "AuthenticationProvider"));
callback(errorObject);
}
};
// API METHODS
// override the prototype's hasClientCredentials method
this.hasClientCredentials = function () {
return (retrieveToken() === null ? false : true);
};
this.refresh = function () {
var deferred = new progress.util.Deferred(),
xhr;
try {
if (!this._loggedIn) {
// "The refresh method was not executed because the AuthenticationProvider is not logged in."
throw new Error(progress.data._getMsgText("jsdoMSG053", "AuthenticationProvider", "refresh"));
}
if (!this.hasRefreshToken()) {
// "Token refresh was not executed because the AuthenticationProvider does not have a
// refresh token."
throw new Error(progress.data._getMsgText("jsdoMSG054", "AuthenticationProvider"));
}
xhr = new XMLHttpRequest();
openRefreshRequest(xhr);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
// process the response from the Web application
try {
processRefreshResult(xhr, deferred);
} catch (e){
}
}
};
xhr.send('{"token_type":"' + retrieveTokenType() + '","refresh_token":"' +
retrieveRefreshToken() + '"}');
} catch (error) {
if (progress.util.Deferred.useJQueryPromises) {
throw error;
} else {
deferred.reject(this, progress.data.Session.GENERAL_FAILURE, {
errorObject: error
});
}
}
return deferred.promise();
};
this.hasRefreshToken = function () {
return (retrieveRefreshToken() === null ? false : true);
};
// PROCESS CONSTRUCTOR ARGUMENTS, CREATE API PROPERTIES, ETC.
this._initialize(uri,
progress.data.Session.AUTH_TYPE_FORM_SSO,
{"_loginURI": progress.data.AuthenticationProvider._springFormTokenLoginURIBase,
"_logoutURI": progress.data.AuthenticationProvider._springLogoutURIBase,
"_refreshURI": progress.data.AuthenticationProvider._springFormTokenRefreshURIBase
});
// in addition to the standard AuthenticationProvider properties, an SSO provider also has a property
// to control automatic token refresh (it's enabled by default on the assumption that developers
// will usually want this)
_automaticTokenRefresh = true;
Object.defineProperty(this, 'automaticTokenRefresh',
{
get: function () {
return _automaticTokenRefresh;
},
set: function (value) {
if (value === true || value === false) {
_automaticTokenRefresh = value;
} else {
throw new Error(progress.data._getMsgText("jsdoMSG061",
"AuthenticationProvider",
"automaticTokenRefresh"));
}
},
enumerable: true
});
// add the automaticTokenRefresh key to the base class's list of data keys
this._dataKeys.automaticTokenRefresh = this._storageKey + ".automaticTokenRefresh";
// set it from storage, if it's in storage
temp = this._retrieveInfoItem(this._dataKeys.automaticTokenRefresh);
if (temp === false) {
_automaticTokenRefresh = false;
}
// We're currently storing the token in storage with the
// uri as the key. This is subject to change later.
tokenDataKeys.token = this._storageKey + tokenDataKeys.token;
tokenDataKeys.refreshToken = this._storageKey + tokenDataKeys.refreshToken;
tokenDataKeys.tokenType = this._storageKey + tokenDataKeys.tokenType;
tokenDataKeys.expiration = this._storageKey + tokenDataKeys.expiration;
tokenDataKeys.accessTokenExpiration = this._storageKey + tokenDataKeys.accessTokenExpiration;
// NOTE: we rely on the prototype's logic to set this._loggedIn. An alternative could be to
// use the presence of a token to determine that, but it's conceivable that we could be
// logged in but for some reason not have a token (e.g., a token expired, or we logged in
// but the authentication server did not return a token)
if (retrieveToken()) {
this._loggedIn = true;
}
// END OF CONSTRUCTOR PROCESSING
};
// END OF AuthenticationProviderSSO CONSTRUCTOR
// NOTE: This is used only for the SSO authentication.
// Define the prototype as an instance of an AuthenticationProviderForm object
function SSOProxy() {}
SSOProxy.prototype = progress.data.AuthenticationProviderForm.prototype;
progress.data.AuthenticationProviderSSO.prototype =
new SSOProxy();
// But reset the constructor back to the SSO constructor (this is pretty much irrelevant,
// though. The correct constructor would be called anyway. It's mainly for the sake of anyone
// wanting to see what the constructor of an object is (maybe a framework)
progress.data.AuthenticationProviderSSO.prototype.constructor =
progress.data.AuthenticationProviderSSO;
// override the base AuthenticationProvider _storeInfo and _clearinfo, but keep refs so they
// can be invoked within the overrides
fn = progress.data.AuthenticationProviderSSO.prototype._storeInfo;
progress.data.AuthenticationProviderSSO.prototype._storeInfo =
function () {
progress.data.AuthenticationProviderSSO.prototype._storeInfo._super.apply(this);
this._storage.setItem(this._dataKeys.automaticTokenRefresh,
JSON.stringify(this._automaticTokenRefresh));
};
progress.data.AuthenticationProviderSSO.prototype._storeInfo._super = fn;
fn = progress.data.AuthenticationProviderSSO.prototype._clearInfo;
progress.data.AuthenticationProviderSSO.prototype._clearInfo =
function () {
progress.data.AuthenticationProviderSSO.prototype._clearInfo._super.apply(this);
this._storage.removeItem(this._dataKeys.automaticTokenRefresh);
};
progress.data.AuthenticationProviderSSO.prototype._clearInfo._super = fn;
// NOTE: There are no overrides of the following methods (either here or in the constructor).
// This object uses these methods from the original prototype(i.e., the implementations from the
// Auth...Form object) because for an OE SSO token server, the login/logout model is Form (the
// only difference is the use of a special URI query string in the login (see the call to
// initialize() in the SSO constructor (above)):
// login (API method)
// _openLoginRequest (API helper method)
// logout (API method)
// _openLogoutRequest (API helper method)
// _processLogoutResult (API helper method)
// NOTE: All overrides are implemented in the constructor (rather than adding them to the prototype)
// because they need access to variables and/or functions that are defined in the constructor
// (in an attempt to protect the token info somewhat)
}());