PageSourceSearch

https://online.roboform.com/js/sso-oidc.js?vvv=202404231653

js roboform.com collected 2026-09-24 08:26:15 UTC 26,612 bytes, 616 lines download raw bytes

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.