1/* global $, RfapiJS, RFMgr, Utils, _STR, RFO */ 2 3let SsoOidc = function (url_parts) 4{ 5 let redirect_url = RFMgr.GetServerUrl()+ '/oidc-callback'; 6 let local_redirect_url = RFMgr.GetServerUrl()+ '/login?authReturnUrl=sso_auth'; 7 let allowed_client_types_by_priority = ['web', 'default']; 8 let allowed_response_modes_by_priority = ['fragment', 'query']; 9 10 let OIDC_CODE_VERIFIER_LENGTH = 64; 11 let OIDC_NONCE_LENGTH = 64; 12 let OIDC_STORAGE_DATA_KEY_LENGTH = 64; 13 let OIDC_STORAGE_KEY = 'encrypted-oidc'; 14 15 let GetSSOKey_ErrorCode = { 16 Failed: 0, 17 NotFound: 1 18 }; 19 20 function trace(msg) { 21 //console.log(new Date().toUTCString() + "\n" + msg); 22 } 23 24 function GetLoginMethod(email) { 25 trace('SsoOidc::GetLoginMethod: ' + email); 26 27 return Utils.createJSONRequest( 28 '/account/get-login-method', 29 {email: email}, 30 {'X-Sib-Client-Type': 'Site'} 31 ); 32 } 33 34 function Auth(login_method, oidcConfig, email, old_password) { 35 trace('SsoOidc::Auth'); 36 37 if (login_method.type !== 'sso' || login_method.sso === undefined) { 38 throw 'Incorrect login method: ' + (login_method.type || '-'); 39 } 40 if (login_method.sso.type !== 'oidc') { 41 throw 'Incorrect SSO configuration: unsupported SSO type ' + (login_method.sso.type || '-'); 42 } 43 if (login_method.sso.oidc === undefined) { 44 throw 'Incorrect SSO configuration: OIDC parameters are not configured'; 45 } 46 47 return _GetOidcConfig(login_method, oidcConfig).then(function (oidc_config) { 48 let code_verifier = _UrlSafeBytesToBase64(RfapiJS.utils.randomBytes(OIDC_CODE_VERIFIER_LENGTH)); 49 let code_challenge = _UrlSafeBytesToBase64(_SHA256(RfapiJS.UTF8.stringToBytes(code_verifier))); 50 let code_challenge_method = 'S256'; 51 52 let nonce = _UrlSafeBytesToBase64(RfapiJS.utils.randomBytes(OIDC_NONCE_LENGTH)); 53 54 let oidc_client_data_json = { 55 email: email, 56 code_verifier: code_verifier, 57 nonce: nonce, 58 oidc_config: JSON.stringify(oidc_config), 59 login_method: JSON.stringify(login_method), 60 old_password: old_password 61 }; 62 63 let oidc_client_key_bytes = RfapiJS.utils.randomBytes(OIDC_STORAGE_DATA_KEY_LENGTH); 64 let encrypted_oidc_client_data_base64 = Utils.encrypt(JSON.stringify(oidc_client_data_json), oidc_client_key_bytes); 65 66 Utils.Storage.set(OIDC_STORAGE_KEY, encrypted_oidc_client_data_base64); 67 68 let state_json = { 69 c: {k: _UrlSafeBytesToBase64(oidc_client_key_bytes)}, 70 s: {u: local_redirect_url} 71 }; 72 73 let state = _UrlSafeBytesToBase64(RfapiJS.UTF8.stringToBytes(JSON.stringify(state_json))); 74 75 let client_id = _CalcualteClientId(allowed_client_types_by_priority, login_method.sso.oidc.clientId); 76 let response_mode = _CalculateResponseMode(allowed_response_modes_by_priority, oidc_config.response_modes_supported); 77 78 let oidc_authorize_params= { 79 'client_id' : client_id, 80 'redirect_uri' : redirect_url, 81 'scope' : login_method.sso.oidc.scope, 82 'response_type' : 'code', 83 'response_mode' : response_mode, 84 'code_challenge_method': code_challenge_method, 85 'code_challenge' : code_challenge, 86 'prompt' : 'login', 87 'state' : state, 88 'nonce' : nonce, 89 'login_hint' : email 90 }; 91 92 document.location.href = oidc_config.authorization_endpoint + "?" + _MakeFormData(oidc_authorize_params); 93 }); 94 } 95 96 function CompleteAuth() { 97 trace('SsoOidc::CompleteAuth'); 98 99 return $.when().then(function() { 100 // get and decrypt oidc_data from storage using key from state 101 102 let auth_code = url_parts.code; 103 let state_base64 = url_parts.state; 104 105 if (url_parts.error || url_parts.error_description) { 106 throw 'Proivder returns error: '+ url_parts.error + (url_parts.error_description ? ', description: ' + url_parts.error_description : ''); 107 } 108 if (!auth_code) { 109 throw 'Proivder returns empty auth code'; 110 } 111 112 let encrypted_oidc_data_base64 = Utils.Storage.get(OIDC_STORAGE_KEY); 113 Utils.Storage.remove(OIDC_STORAGE_KEY); 114 if (typeof encrypted_oidc_data_base64 !== 'string') { 115 throw 'OIDC login: lost data'; 116 } 117 118 let state = Utils.parseJSON(RfapiJS.UTF8.bytesToString(_UrlSafeBase64ToBytes(state_base64))); 119 if (!state || !state.c || !state.c.k || typeof state.c.k !== 'string') { 120 throw 'Proivder returns incorrect state'; 121 } 122 123 let decrypted_oidc_data = Utils.decrypt(encrypted_oidc_data_base64, _UrlSafeBase64ToBytes(state.c.k)); 124 let oidc_data = Utils.parseJSON(decrypted_oidc_data); 125 if (oidc_data.code_verifier === undefined 126 || oidc_data.nonce === undefined 127 || oidc_data.email === undefined 128 || oidc_data.login_method === undefined 129 || oidc_data.oidc_config === undefined 130 ) { 131 throw 'OIDC login: incorrect data'; 132 } 133 134 let oidc_config = Utils.parseJSON(oidc_data.oidc_config); 135 if (!oidc_config.token_endpoint) { 136 throw 'OIDC login: incorrect token endpoint';
137 } 138 139 let login_method = Utils.parseJSON(oidc_data.login_method); 140 let password_storage = login_method && login_method.sso && login_method.sso.passwordStorage || {}; 141 142 let client_id; 143 if (login_method.sso && login_method.sso.oidc && login_method.sso.oidc.clientId && login_method.sso.oidc.clientId.default !== undefined) { 144 client_id = login_method.sso.oidc.clientId.default; 145 } else if (login_method.sso && login_method.sso.oidc && login_method.sso.oidc.clientId && login_method.sso.oidc.clientId.web !== undefined) { 146 client_id = login_method.sso.oidc.clientId.web; 147 } else { 148 throw 'OIDC login: incorrect login method'; 149 } 150 151 return _GetIdpTokens(oidc_config.token_endpoint, client_id, auth_code, oidc_data.code_verifier).then(function (token) { 152 if (oidc_data.old_password) { 153 return _ChangeLoginMethodToSSO(login_method, token, oidc_data).then(function (password) { 154 return { 155 email: oidc_data.email, 156 password: password 157 }; 158 }); 159 } 160 return $.when(_GetRfoKey(token), _GetIdpKey(token, password_storage)).then(function (rfo_key, idp_key) { 161 return { 162 email: oidc_data.email, 163 password: _ComputeMasterPassword(rfo_key, idp_key) 164 }; 165 }).catch(function (e) { 166 throw e.message || e; 167 }); 168 }); 169 }); 170 } 171 172 function _SetLoginMethod(login_method, new_password, oidc_data, sso_key, id_token) { 173 trace('SsoOidc::_SetLoginMethod'); 174 175 let def = $.Deferred(); 176 177 RFMgr.SetUserID(oidc_data.email); 178 RFMgr.setMasterPwd(oidc_data.old_password); 179 RFMgr.setAccountPwd(oidc_data.old_password); 180 RFMgr.SetLoginMethod(login_method, new_password, oidc_data.old_password, sso_key, id_token, 181 function () { 182 def.resolve(new_password); 183 }, 184 function (errObject) { 185 def.reject(Utils.getErrorDescription(errObject)); 186 } 187 ); 188 return def.promise(); 189 } 190 191 function _ChangeLoginMethodToSSO(new_login_method, token, oidc_data) { 192 trace('SsoOidc::_ChangeLoginMethodToSSO'); 193 194 return $.when().then(function() { 195 if (new_login_method.type !== 'sso' || !new_login_method.sso) { 196 throw "Change login method to SSO was called, but the new login method isn't SSO"; 197 } 198 199 let rfo_key = RfapiJS.utils.generatePassword(40, true, true, true); 200 201 // Get idp_key from storage or generate it and set to storage 202 return _GetIdpKey(token, new_login_method.sso.passwordStorage) 203 .catch(function (error) { 204 let promise; 205 switch (error.ssoGetKeyError) { 206 case GetSSOKey_ErrorCode.Failed: 207 // Cannot get SSO key, storage server returned error or wrong data. 208 // Example: we can have storage data without key field in Azure. 209 // Cleanup storage, then generate and store new key. 210 promise = _DeleteIdpKey(token, new_login_method.sso.passwordStorage).catch(function (e) { 211 // 'catch' - process as ok to do nothing if delete not happens 212 return e; 213 }); 214 break; 215 case GetSSOKey_ErrorCode.NotFound: 216 // Can access external storage but SSO key is missing there. 217 // Generate new one and send it to storage. 218 promise = $.Deferred().resolve(); 219 break; 220 default: 221 throw error.message || error; 222 } 223 return promise.then(function () { 224 let idp_key = RfapiJS.utils.generatePassword(40, true, true, true); 225 return _SetIdpKey(token, new_login_method.sso.passwordStorage, idp_key).then(function () { 226 return idp_key; 227 }); 228 }); 229 })
230 .then(function (idp_key) { 231 let new_password = _ComputeMasterPassword(rfo_key, idp_key); 232 return _SetLoginMethod(new_login_method, new_password, oidc_data, rfo_key, token.id_token); 233 }); 234 }); 235 } 236 237 function _GetRfoKey(token) { 238 trace('SsoOidc::_GetRfoKey'); 239 240 return Utils.createJSONRequest('/account/get-sso-key', {idToken: token.id_token}).then(function (response) { 241 return response.key; 242 }); 243 } 244 245 function _GetIdpKey(token, password_storage) { 246 trace('SsoOidc::_GetIdpKey'); 247 248 return $.when().then(function() { 249 if (password_storage.type === 'key1rfo-key2external') { 250 let key_get = password_storage.externalKey && password_storage.externalKey.get; 251 if (!key_get || !key_get.requestTemplate || !key_get.responseTraits) { 252 throw 'Incorrect SSO configuration: get method is not specified in key storage configuration'; 253 } 254 return _GetSsoIdpKey(key_get.requestTemplate, { 255 access_token: token.access_token, 256 id_token: token.id_token, 257 access_token_payload: token.access_token_payload, 258 id_token_payload: token.id_token_payload 259 }, key_get.responseTraits).then(function (key) { 260 return key; 261 }); 262 } else if (password_storage.type === 'key1rfo-key2token') { 263 throw 'Incorrect SSO configuration: this password storage is not implemented'; 264 } else { 265 throw 'Incorrect SSO configuration: unknown password storage type [' + password_storage.type + ']'; 266 } 267 }); 268 } 269 270 function _GetSsoIdpKey(request_template, template_values, response_traits) { 271 trace('SsoOidc::_GetSsoIdpKey'); 272 273 let def = $.Deferred(); 274 275 if (response_traits.jsonKeyPath === undefined) { 276 return def.reject('Cannot get SSO key from provider: wrong SSO configuration, responseTraits not configured'); 277 } 278 try { 279 let request = _MakeRequestByTemplate(request_template, template_values); 280 request.receiveBodyType = 'json'; 281 request.success = function (response) { 282 let idp_key = _GetJsonValueByPath(response, response_traits.jsonKeyPath); 283 if (typeof idp_key !== 'string' || idp_key === '') { 284 if (response_traits.keyNotFoundIf && response_traits.keyNotFoundIf.missingKeyField === true) { 285 return def.reject({ 286 message: 'Cannot get SSO key from provider: not found in storage', 287 ssoGetKeyError: GetSSOKey_ErrorCode.NotFound 288 }); 289 } else { 290 return def.reject({ 291 message: 'Cannot get SSO key from provider: got wrong data from server', 292 ssoGetKeyError: GetSSOKey_ErrorCode.Failed 293 }); 294 } 295 } 296 def.resolve(idp_key); 297 }; 298 request.error = function (xhr) { 299 let error = { 300 message: 'Cannot get SSO key from provider' + _GetHttpError(xhr) 301 }; 302 if (response_traits.keyNotFoundIf && response_traits.keyNotFoundIf.errorHttpResponses) { 303 response_traits.keyNotFoundIf.errorHttpResponses.forEach(function (key_not_found_error_http_response) { 304 if (key_not_found_error_http_response.status === xhr.status) { 305 error.ssoGetKeyError = GetSSOKey_ErrorCode.NotFound; 306 } 307 }); 308 } 309 return def.reject(error); 310 }; 311 $.ajax(request); 312 } catch (e) { 313 return def.reject(e); 314 } 315 return def.promise(); 316 } 317 318 function _SetIdpKey(token, password_storage, key) { 319 trace('SsoOidc::_SetIdpKey'); 320 321 return $.when().then(function() { 322 if (password_storage.type === 'key1rfo-key2external') { 323 let key_set = password_storage.externalKey && password_storage.externalKey.set; 324 if (!key_set || !key_set.requestTemplate) { 325 throw 'Incorrect SSO configuration: set method is not specified in key storage configuration'; 326 } 327 return _SetSsoIdpKey(key_set.requestTemplate, { 328 random_generated_key: key, 329 access_token: token.access_token, 330 id_token: token.id_token, 331 access_token_payload: token.access_token_payload, 332 id_token_payload: token.id_token_payload 333 }); 334 } else if (password_storage.type === 'key1rfo-key2token') { 335 // Key is company-wide if it's in token and we can't Set it. It's happen by company admin action 336 } else { 337 throw 'Incorrect SSO configuration: unknown password storage type [' + password_storage.type + ']'; 338 } 339 }); 340 } 341 342 function _SetSsoIdpKey(request_template, template_values) { 343 trace('SsoOidc::_SetSsoIdpKey'); 344 345 let def = $.Deferred(); 346 try { 347 let request = _MakeRequestByTemplate(request_template, template_values); 348 request.success = def.resolve; 349 request.error = function (xhr) { 350 def.reject('Cannot store SSO key to provider' + _GetHttpError(xhr)); 351 }; 352 $.ajax(request); 353 } catch (e) { 354 def.reject(e); 355 } 356 return def.promise(); 357 } 358 359 function _DeleteIdpKey(token, password_storage) { 360 trace('SsoOidc::_DeleteIdpKey'); 361 362 return $.when().then(function() { 363 if (password_storage.type === 'key1rfo-key2external') { 364 let delete_key = password_storage.externalKey && password_storage.externalKey.delete; 365 if (!delete_key || !delete_key.requestTemplate) { 366 throw 'Incorrect SSO configuration: delete method is not specified in key storage configuration'; 367 } 368 return _DeleteSsoIdpKey(delete_key.requestTemplate, { 369 access_token: token.access_token, 370 id_token: token.id_token, 371 access_token_payload: token.access_token_payload, 372 id_token_payload: token.id_token_payload 373 }); 374 } else if (password_storage.type === 'key1rfo-key2token') { 375 // Cannot set key in this mode. 376 // One key is shared between all company members and can be set by company admin 377 } else { 378 throw 'Incorrect SSO configuration: unknown password storage type [' + password_storage.type + ']'; 379 } 380 }); 381 } 382 383 function _DeleteSsoIdpKey(request_template, template_values) { 384 trace('SsoOidc::_DeleteSsoIdpKey'); 385 386 let def = $.Deferred(); 387 try { 388 let request = _MakeRequestByTemplate(request_template, template_values); 389 request.success = def.resolve; 390 request.error = function (xhr) { 391 def.reject('Cannot delete SSO key from provider' + _GetHttpError(xhr)); 392 }; 393 $.ajax(request); 394 } catch (e) { 395 def.reject(e); 396 } 397 return def.promise(); 398 } 399 400 function _GetIdpTokens(token_url, client_id, auth_code, code_verifier) { 401 trace('SsoOidc::_GetIdpTokens'); 402 403 let request = $.Deferred(); 404 let params = { 405 client_id : client_id, 406 code : auth_code, 407 code_verifier : code_verifier, 408 grant_type : 'authorization_code', 409 redirect_uri : redirect_url 410 }; 411 $.post(token_url, params, function (data) { 412 let access_token_payload = Utils.parseJSON(RfapiJS.UTF8.bytesToString(_UrlSafeBase64ToBytes(data.access_token.split('.')[1]))); 413 if (access_token_payload === false) { 414 return request.reject('Bad access token payload'); 415 } 416 let id_token_payload = Utils.parseJSON(RfapiJS.UTF8.bytesToString(_UrlSafeBase64ToBytes(data.id_token.split('.')[1]))); 417 if (id_token_payload === false) { 418 return request.reject('Bad ID token payload'); 419 } 420 request.resolve({ 421 id_token: data.id_token, 422 id_token_payload: id_token_payload, 423 access_token: data.access_token, 424 access_token_payload: access_token_payload, 425 }); 426 }, 'json').fail(function (e) { 427 request.reject('Cannot get tokens from provider' + _GetHttpError(e)); 428 }); 429 return request.promise(); 430 } 431 432 function _GetHttpError(xhr) { 433 let msg = (xhr.statusText || '') + ' (' + (xhr.status || '') + ')'; 434 if (xhr.responseJSON && xhr.responseJSON.error && xhr.responseJSON.error.message) { 435 msg = xhr.responseJSON.error.message; 436 } else if (!xhr.responseJSON && xhr.responseText) { 437 msg = xhr.responseText; 438 } 439 return ': ' + msg; 440 } 441 442 function _GetOidcConfig(login_method, oidcConfig) { 443 trace('SsoOidc:_GetOidcConfig'); 444 445 let request = $.Deferred(); 446 let config = login_method.sso && login_method.sso.oidc && login_method.sso.oidc.config || {}; 447 if (oidcConfig) { 448 request.resolve(oidcConfig); 449 } else if (config.url) { 450 $.get(login_method.sso.oidc.config.url, function (data) { 451 request.resolve(data); 452 }, 'json').fail(function (e) { 453 request.reject('Cannot get oidc config from provider' + _GetHttpError(e)); 454 }); 455 } else if (config.json) { 456 request.resolve(login_method.sso.oidc.config.json); 457 } else { 458 request.reject('Incorrect SSO configuration: OIDC configuration document is not specified'); 459 } 460 return request.promise(); 461 } 462 463 function _ComputeMasterPassword(rfo_key, idp_key) { 464 return rfo_key + idp_key; 465 } 466 467 function _CalcualteClientId(allowed_client_types_by_priority, client_id) { 468 for (let i = 0; i < allowed_client_types_by_priority.length; i++) { 469 let current_client_id = client_id[allowed_client_types_by_priority[i]]; 470 if (current_client_id !== undefined && current_client_id !== '') { 471 return current_client_id; 472 } 473 } 474 throw 'Incorrect SSO configuration: this client is not registered with the provider'; 475 } 476 477 function _CalculateResponseMode(allowed_response_modes_by_priority, supported_response_mode) { 478 for (let i = 0; i < allowed_response_modes_by_priority.length; i++) { 479 if (Utils.in_array(allowed_response_modes_by_priority[i], supported_response_mode)) { 480 return allowed_response_modes_by_priority[i]; 481 } 482 } 483 throw 'Incorrect SSO configuration: provider does not support OIDC response mode used by client'; 484 } 485 486 function _UrlSafeBytesToBase64(bytes) { 487 let g_re_replace_all_eq_chars = /=/g; 488 let g_re_replace_all_plus_chars = /\+/g; 489 let g_re_replace_all_slash_chars = /\//g; 490 let base64 = RfapiJS.utils.bytesToBase64(bytes); 491 return base64 492 .replace(g_re_replace_all_eq_chars, "") 493 .replace(g_re_replace_all_plus_chars, "-") 494 .replace(g_re_replace_all_slash_chars, "_"); 495 } 496 497 function _UrlSafeBase64ToBytes(base64_string) { 498 if (!String.prototype.repeat) { 499 String.prototype.repeat = function (count) { 500 let str = '' + this; 501 count = +count; 502 let maxCount = str.length * count; 503 count = Math.floor(Math.log(count) / Math.log(2)); 504 while (count) { 505 str += str; 506 count--; 507 } 508 str += str.substring(0, maxCount - str.length); 509 return str; 510 } 511 } 512 if (!String.prototype.padEnd) { 513 String.prototype.padEnd = function (targetLength, padString) { 514 targetLength = targetLength >> 0; //truncate if number or convert non-number to 0; 515 padString = String((typeof padString !== 'undefined' ? padString : ' ')); 516 if (this.length > targetLength) { 517 return String(this); 518 } else { 519 targetLength = targetLength - this.length; 520 if (targetLength > padString.length) { 521 padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed 522 } 523 return String(this) + padString.slice(0, targetLength); 524 } 525 }; 526 } 527 base64_string = base64_string.replace(/-/g, "+").replace(/_/g, "/"); 528 let padding = (4 - (base64_string.length % 4)) % 4; 529 base64_string = base64_string.padEnd(base64_string.length + padding, "="); 530 return RfapiJS.utils.base64ToBytes(base64_string); 531 } 532 533 function _SHA256(bytes) { 534 let base64 = RfapiJS.utils.bytesToBase64(bytes); 535 let words = CryptoJS.enc.Base64.parse(base64); 536 let hash_words = CryptoJS.SHA256(words); 537 return _ConvertCryptoJSWordsToBytes(hash_words); 538 } 539 540 function _ConvertCryptoJSWordsToBytes(words_array) { 541 let words = words_array.words; 542 let sig_bytes = words_array.sigBytes; 543 let u8 = new Uint8Array(sig_bytes); 544 for (let i = 0; i < sig_bytes; i++) { 545 u8[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
546 } 547 return u8; 548 } 549 550 function _MakeRequestByTemplate(request_template, template_values) { 551 let _SetTemplateValues = function (template_field) { 552 return template_field.replace(/%(\w+(\.\w+)*)%/g, function (_, key_to_replace) { 553 let value, first_dot_pos = key_to_replace.indexOf('.'); 554 if (first_dot_pos !== -1) { 555 let obj = template_values[key_to_replace.substring(0, first_dot_pos)]; 556 let obj_value_path = key_to_replace.substring(first_dot_pos + 1); 557 value = _GetJsonValueByPath(obj, obj_value_path); 558 } else { 559 value = template_values[key_to_replace]; 560 } 561 if (typeof value !== 'string') { 562 throw "Incorrect login_method, can't get %" + key_to_replace + "%"; 563 } 564 return value; 565 }); 566 } 567 568 let url = _SetTemplateValues(request_template.url); 569 let method = request_template.method; 570 571 let headers = {}; 572 request_template.headers.forEach(function (header) { 573 headers[header[0]] = _SetTemplateValues(header[1]); 574 }); 575 576 let body = null; 577 if (request_template.body !== undefined && request_template.body !== '') { 578 body = _SetTemplateValues(request_template.body); 579 } 580 581 return { 582 url: url, 583 method: method, 584 headers: headers, 585 data: body 586 }; 587 } 588 589 function _GetJsonValueByPath(obj, path) { 590 let keys = path.split('.'); 591 let current = obj; 592 for (let i = 0; i < keys.length; i++) { 593 let key = keys[i]; 594 if (current === null || current === undefined || !(key in current)) { 595 return undefined; 596 } 597 current = current[key]; 598 } 599 return current; 600 } 601 602 function _MakeFormData(form_data) { 603 let url_extra = ""; 604 for (let key in form_data) { 605 let value = form_data[key]; 606 url_extra += (url_extra !== '' ? '&' : '') + encodeURIComponent(key) + (value !== true ? '=' + encodeURIComponent(value) : ''); 607 } 608 return url_extra; 609 } 610 611 return { 612 GetLoginMethod: GetLoginMethod, 613 Auth: Auth, 614 CompleteAuth: CompleteAuth 615 } 616};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.