UNPKG

oidc-lib

Version:

A library for creating OIDC Service Providers

1,727 lines (1,485 loc) 62.7 kB
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ module.exports = { "email": { "input": "fieldset", "content": { "email": { "label": "Email address", "input": "email" }, "email_verified": { "label": "Verified", "input": "radio", "values": [ "true", "false" ], "default": "false", "editable": false } } }, "phone": { "input": "fieldset", "content": { "phone_number": { "label": "Phone", "input": "text" }, "phone_number_verified": { "input": "radio", "label": "Verified", "values": [ "true", "false" ], "default": "false", "editable": false } } }, "address": { "input": "fieldset", "legend": "Postal Address", "object": "address", "content": { "street_address": { "label": "Address", "input": "text" }, "street_address_1": { "label": "Apt or Unit", "input": "text" }, "locality": { "label": "City", "input": "text" }, "region": { "label": "State", "input": "text" }, "postal_code": { "label": "Zip", "input": "text" }, "country": { "label": "Country", "input": "text" }, "address": { "input": "object", "note": "contains all claims with object of \"address\"" } } }, "profile": { "input": "fieldset", "legend": "Profile", "content": { "name": { "label": "Name", "input": "text" }, "given_name": { "label": "First Name", "input": "text" }, "family_name": { "label": "Family Name", "input": "text" }, "middle_name": { "label": "Middle Name", "input": "text" }, "nickname": { "label": "Nickname", "input": "text" }, "preferred_username": { "label": "Username", "input": "text" }, "profile": { "label": "Profile", "input": "text" }, "picture": { "label": "Picture", "input": "photo" }, "website": { "label": "Web Site", "input": "text" }, "gender": { "label": "Gender", "input": "radio", "values": [ "male", "female", "alternative" ], "default": "female" }, "birthdate": { "label": "Date of Birth", "input": "text", "template": "YYYY-MM-DD" }, "zoneinfo": { "label": "Zone Info", "input": "text" }, "locale": { "label": "Locale", "input": "text" }, "updated_at": { "input": "text", "format": "date" } } } } },{}],2:[function(require,module,exports){ module.exports = { loadFormValues: loadFormValues, formToJson: formToJson, createInputDiv: createInputDiv, displayError: displayError, clearError: clearError, postJsonData: postJsonData } function loadFormValues(formTemplate, formValues){ clearError(); for (var parameter in formTemplate){ var definition = formTemplate[parameter]; var value = ''; var id = ''; var el; if (formValues[parameter] === undefined){ if (definition.default === 'suppress!'){ continue; } if (definition.default !== undefined){ value = definition.default; } } else{ value = formValues[parameter]; } if (value === undefined){ continue; } switch (definition.input){ case 'text': // text may be a simple text placed into a text box // or an array of text placed into a textarea // or an array of text placed into a text box... (e.g. scope) el = document.getElementById(parameter); if (typeof value === 'string'){ el.value = value; } else if (Array.isArray(value)){ if (value.length === undefined){ continue; } var valueSeparator = ' '; if (definition.type === 'array') valueSeparator = '\n'; var content = ''; var separator = ''; for (var i=0; i<value.length; i++){ var line = value[i].trim(); if (line.length > 0){ content += separator + line; separator = valueSeparator; } } el.value = content; } else { el.value = JSON.stringify(value); } break; case 'select': el = document.getElementById(parameter); el.value = value; break; case 'boolean': case 'radio': if (value){ id = parameter + '_' + value; el = document.getElementById(id); el.checked = true; } break; case 'checkboxes': if (value && value.length > 0){ for (var i=0; i < value.length; i++){ id = parameter + '_' + value[i]; el = document.getElementById(id); el.checked = true; } } break; case 'checkbox': if (value){ el = document.getElementById(parameter); el.checked = true; } break; case 'fieldset': loadFormValues(definition.content, formValues); break; case 'hidden': el = document.getElementById(parameter); el.value = value; default: break; } } } function flattenTemplates(result, template) { var definition; for (var parameter in template){ definition = template[parameter]; if (definition.input === 'fieldset'){ flattenTemplates(result, definition.content); } else{ result[parameter] = definition; } } } function formToJson(formId, formTemplate){ var flatTemplate = {}; flattenTemplates(flatTemplate, formTemplate); var formEl = document.getElementById(formId); var formInputs = formEl.getElementsByTagName('input'); var formTextAreas = formEl.getElementsByTagName('textarea'); var formSelects = formEl.getElementsByTagName('select'); var result = {}; for (var i=0; i < formInputs.length; i++){ var input_el = formInputs[i]; var name = input_el.name; var definition = flatTemplate[name]; if (definition === undefined) continue; switch (input_el.type){ case 'hidden': case 'text': case 'select': if (input_el.value.length !== 0) result[name] = input_el.value; break; case 'boolean': case 'radio': if (input_el.checked){ var prefix = name + '_'; value = input_el.id.substring(prefix.length); result[name] = value; } break; case 'checkbox': if (input_el.checked){ var prefix = name + '_'; if (input_el.id.startsWith(prefix)){ if (result[name] === undefined){ result[name] = []; } value = input_el.id.substring(prefix.length); result[name].push(value); } else{ result[name] = true; } } break; default: break; } } for (var i=0; i < formSelects.length; i++){ var select_el = formSelects[i]; var name = select_el.name; var definition = flatTemplate[name]; if (definition === undefined) continue; if (select_el.value.length !== 0) result[name] = select_el.value; } for (var i=0; i < formTextAreas.length; i++){ var textArea_el = formTextAreas[i]; var name = textArea_el.name; var definition = flatTemplate[name]; if (definition === undefined) continue; if (textArea_el.value.length !== 0){ if (definition.type === 'array'){ var valueArray = []; var text = textArea_el.value; var textArray = text.split('\n'); for (var j=0; j < textArray.length; j++){ if (textArray[j].length === 0) continue; valueArray.push(textArray[j]); } result[name] = valueArray; } else{ result[name] = textArea_el.value; } } } return result; } function createInputDiv(formDivId, formTemplate, formValues, scopes){ form_html = generateInputGroupingHtml(formTemplate, formValues); var form_el = document.getElementById(formDivId); form_el.innerHTML = form_html; } function generateInputGroupingHtml(formTemplate, formValues, scopes){ var label, formGroup, panel, value, id; var grouping_html = ''; for (var parameter in formTemplate){ var param_html = ''; var definition = formTemplate[parameter]; if (formValues[parameter] === undefined){ if (definition.default === 'suppress!'){ continue; } } switch (definition.input){ case 'text': case 'email': formGroup = createFormGroup(definition, parameter); if (definition.type === undefined || definition.type === 'string'){ param_html = formGroup + '<input type="text" id="' + parameter + '" name="' + parameter + '" class="form-control"/>\r\n'; } else{ param_html = formGroup + '<textarea rows="2" cols="60" wrap="hard" id="' + parameter + '" name="' + parameter + '" style="height: auto;"></textarea>\r\n'; } param_html += closeFormGroup(formGroup); break; case 'radio': checkValuesExist(parameter, definition); var divId = 'radio_' + parameter; formGroup = createFormGroup(definition, divId); param_html = formGroup + '<div id="' + divId + '">'; for (var i=0; i < definition.values.length; i++){ value = definition.values[i]; id = parameter + '_' + value; param_html += '<label class="radio-inline"><input type="radio" name="' + parameter + '" id="' + id + '" />' + value + '</label>'; } param_html += '</div>' + closeFormGroup(formGroup); break; case 'select': checkValuesExist(parameter, definition); label = createLabel(definition, parameter); param_html = label + '<select id="' + parameter + '" name="' + parameter + '" >\r\n'; for (var i=0; i < definition.values.length; i++){ value = definition.values[i]; param_html += '<option value="' + value + '">' + value + '</option>\r\n'; } param_html += "</select>\r\n"; break; // <div class="form-group form-check"> // <label class="form-check-label"> // <input class="form-check-input" type="checkbox"> Remember me // </label> // </div> case 'checkboxes': checkValuesExist(parameter, definition); panel = createPanelWithHeading(definition, parameter); param_html = panel + '<div class="form-group form-check">\r\n'; for (var i=0; i < definition.values.length; i++){ value = definition.values[i]; id = parameter + '_' + value; param_html += '<label class="form-check-label"><input class="form-check-input" id="' + id + '" name="' + parameter + '" type="checkbox">' + value + '</label>\r\n'; } param_html += closePanel(panel); break; case 'checkbox': label = createLabel(definition, parameter); param_html = '<label class="form-check-label"><input class="form-check-input" id="' + parameter + '" name="' + parameter + '" type="checkbox">' + label + '</label>\r\n'; break; case 'boolean': panel = createPanelWithHeading(definition, parameter); var bool_values = ['true', 'false']; for (var i=0; i < bool_values.length; i++){ value = bool_values[i]; id = parameter + '_' + value; param_html += '<input type="radio" name="' + parameter + '" id="' + id + '" />\r\n' + '<label for="' + id + '" class="inline">' + value + '&nbsp;&nbsp;</label>\r\n'; } param_html += closePanel(panel); break; case 'fieldset': if (scopes !== undefined && definition.scope !== undefined){ if (scopes.indexOf(definition.scope) < 0){ param_html = ''; break; } } param_html = generateInputGroupingHtml(definition.content, formValues); if (param_html.length > 0){ if (definition.legend !== undefined){ panel = createPanelWithHeading(definition, parameter); param_html = panel + param_html + closePanel(panel); } } break; case 'button': label = 'Submit'; if (definition.label !== undefined){ label = definition.label; } param_html = '<div class="col_3"><input type="button" value="' + label + '" onclick="' + definition.method + '" /></div>\r\n'; break; case 'hidden': param_html = '<input type="hidden" id="' + parameter + '" name="' + parameter + '"/>'; break; default: // throw 'undefined input type in reg_parameters: ' + definition.input break; } grouping_html += param_html; } return grouping_html; } function checkValuesExist(parameter, definition){ if (definition.values === undefined){ alert('formTemplate ' + definition.input + ' "values" for "' + parameter + '" is not defined'); throw 'error: values missing'; } } function createLabel(definition, parameter){ var text = parameter; if (definition.label !== undefined) text = definition.label; var label_class = ''; if (definition.label_class !== undefined) label_class = ' class="' + definition.label_class + '"'; return '<label for="' + parameter + '"' + label_class + '>' + text + '</label>\r\n'; } function createPanelWithHeading(definition, parameter){ var text = ''; if (definition.legend !== undefined) text = '<div class="panel panel-default">\r\n<div class="panel-heading">' + definition.legend + '</div>\r\n<div class="panel-body">'; return text; } function closePanel(panel){ var text = ''; if (panel !== '') text = '</div>\r\n</div>\r\n'; return text; } function createFormGroup(definition, parameter){ var label = createLabel(definition, parameter); return '<div class="form-group">' + label; } function closeFormGroup(formGroup){ return '</div>' } function postJsonData(options, jsonData, callback){ var xhr = new XMLHttpRequest(); var url = options.url; xhr.open(options.method, options.url, true); if (options.headers !== undefined){ for (var i=0; i < options.headers.length; i++){ var header = options.headers[i]; xhr.setRequestHeader(header.name, header.value); } } xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status === 200) { var json = JSON.parse(xhr.responseText); callback(json); } }; var data = JSON.stringify(jsonData); xhr.send(data); } function displayError(content){ var warning_text_el = document.getElementById("warning_text"); var warning_div_el = document.getElementById("warning_div"); if (warning_text_el !== undefined) warning_text_el.innerHTML = content; if (warning_div_el !== undefined) warning_div_el.style.visibility = "visible"; } function clearError(content){ var warning_text_el = document.getElementById("warning_text"); var warning_div_el = document.getElementById("warning_div"); if (warning_text_el !== undefined) warning_text_el.innerHTML = ''; if (warning_div_el !== undefined) warning_div_el.style.visibility = "hidden"; } function isset(param){ if (typeof param !== 'object'){ if (param === undefined || param === null || param.length === 0) return false; } return true; } },{}],3:[function(require,module,exports){ const VIEWPATH = '\\wallet\\views\\'; module.exports = { registerEndpoints: registerEndpoints, invokeAuthUserAgent: invokeAuthUserAgent, invokeConsentUserAgent: invokeConsentUserAgent, generateUserinfo: generateUserinfo, processVerifiedIdToken: processVerifiedIdToken, token_presentation_options: token_presentation_options, setConsentMode: setConsentMode, VIEWPATH: VIEWPATH } const moduleName = 'wallet'; var viewPath = null; var pk = null; // var userAccounts = null; var dbInfo = null; var token_presentation_values = { imposeFormPostResponseMode: false, vcFormat: 'verifiablePresentation' }; function registerEndpoints(pkInput) { pk = pkInput; viewPath = VIEWPATH; // userAccounts = require('./data/accounts'); // var claimsMgr = require("./claimsmgr"); // claimsMgr.registerEndpoints(pk, userAccounts, getAccountBySub); pk.app.post('/wallet/auth_useragent_response', function(req, res){ processAuthUserAgentResponse(pk, req, res); }); pk.app.post('/wallet/consent_useragent_response', function(req, res){ processConsentUserAgentResponse(pk, req, res); }); pk.app.post('/wallet/process_ajax_request', function(req, res){ processAjaxRequest(pk, req, res); }); pk.app.get('/wallet/manager', function(req, res){ pk.pmanager.manager(pk, req, res); }); pk.app.get('/wallet/oauth_error', function(req, res){ pk.pmanager.oauth_error(pk, req, res); }); pk.app.get('/wallet/pickup_uri', function(req, res){ process_pickup_uri(pk, req, res); }); pk.app.get('/wallet/entry_point', function(req, res){ return process_wallet_entry_point(pk, req, res); }); pk.app.get('/wallet/oauth_error', function(req, res){ pk.pmanager.oauth_error(pk, req, res); }); // IMPORTANT // Additional 'wallet' endpoints are defined in // walletManager.js } function invokeAuthUserAgent(req, res, params, encoded_sts_state_bundle) { pk.util.log_debug('--- WALLET: INVOKE AUTH USERAGENT ---'); var content_module_state = {}; pk.util.log_debug("WARNING: login_hint has been disabled..."); /* var validLoginHint = false; if (params.login_hint){ var hint_persona = pk.ptools.getPersona('id', params.login_hint); if (hint_persona){ content_module_state.sub = hint_persona.id; validLoginHint = true; } } if (!validLoginHint){ content_module_state.sub = natural.id; } */ pk.sts.applyAuthResponse(res, encoded_sts_state_bundle, content_module_state); return; } /* placeholder if authUserAgentValidation required function processAuthUserAgentResponse (pk, req, res){ pk.sts.applyAuthResponse(res, req.body.encoded_sts_state_bundle, content_module_state); } */ function processVerifiedIdToken(res, verified_id_token, encoded_sts_state_bundle){ pk.util.log_debug('--- WALLET: PROCESS VERIFIED ID TOKEN ---'); var account = getAccountBySub(verified_id_token.sub); if (account === false){ return; } content_module_state = {}; content_module_state.sub = verified_id_token.sub; pk.sts.sendAuthResponse(res, encoded_sts_state_bundle, content_module_state); } async function updateConsentedToken(res, scopeInfo, encoded_sts_state_bundle, content_module_state){ var consentedPersona; var config = pk.ptools.getPersona('natural').data.options; if (!content_module_state.explicitConsent && config.share_consented_claims && content_module_state.consentInfo && content_module_state.consentInfo.idTokenContent && pk.sts.isScopeConsented(content_module_state)){ var id = content_module_state.consentInfo.credentialIssuerId; consentedPersona = pk.ptools.getPersona('id', id); } if (!consentedPersona){ return false; } var claims = content_module_state.consentInfo.idTokenContent; content_module_state.consentInfo = scopeInfo; content_module_state.consentInfo.currentPersona = content_module_state.sub; var requestOptions = { sub: content_module_state.sub } var distributedClaims = await pk.token.retrieveVerifiableCredential(consentedPersona, requestOptions); if (consentedPersona.vc_constants && distributedClaims){ var vcFormat = await pk.feature_modules['wallet'].code.token_presentation_options({ option: 'vcFormat' }); if (vcFormat === 'verifiablePresentation'){ var vp = { '@options': consentedPersona.vc_constants['@options'], type: [ 'VerifiablePresentation'], verifiableCredential: [ distributedClaims.JWT ] } claims['vp'] = vp; } else { claims._claim_names = {}; claims._claim_sources = {}; var effective_class = consentedPersona.vc_constants.type[consentedPersona.vc_constants.type.length - 1]; claims._claim_names[effective_class] = 'vc1'; claims._claim_sources['vc1'] = distributedClaims; } } content_module_state.scope_claim_map = consentedPersona.scope_claim_map; for (var i=0; i<consentedPersona.scopes_and_creds.length; i++){ var scred = consentedPersona.scopes_and_creds[i]; if (content_module_state.consentInfo.scopeArray.includes(scred)){ continue; } content_module_state.consentInfo.scopeArray.push(scred); } content_module_state.consentInfo.credentialIssuerId = consentedPersona.id; claims.sub = content_module_state.consentInfo.currentPersona; content_module_state.newIdTokenContent = claims; pk.sts.applyConsentResponse(res, encoded_sts_state_bundle, content_module_state); return (true); } async function invokeConsentUserAgent(res, scopeInfo, client_info, encoded_sts_state_bundle, content_module_state) { pk.util.log_debug('--- WALLET: INVOKE CONSENT USERAGENT ---'); // skip the UI experience if previous approval has been obtained // the user has configured the wallet for this and the RP is good with it if (await updateConsentedToken(res, scopeInfo, encoded_sts_state_bundle, content_module_state)){ return; } var client_info_class = 'clms_0'; var client_name_warning_class = 'clms_0'; // client_info will be set to null rather than undefined if startup was for wallet/applicaiton if (client_info !== null){ client_info_class = 'clms_1'; var client_uri = client_info.client_uri; if (client_uri === undefined){ client_uri = client_info.redirect_uri; } var client_name = client_info.client_name; if (client_name === undefined){ client_name = client_uri } var company_logo = client_info.company_logo; if (company_logo === undefined){ company_logo = "https://undefined.logo.uri"; // TODO: define default } var client_name_warning = false; var redirect_host = pk.util.url(client_info.redirect_uri); var client_host; if (client_info.client_uri !== undefined){ client_host = pk.util.url(client_info.client_uri); } if (client_host === undefined || client_host.hostname !== redirect_host.hostname){ client_uri = client_info.redirect_uri; client_name_warning = true; } } var scopeInfoString = JSON.stringify(scopeInfo); var scopeInfo64 = pk.base64url.encode(scopeInfoString); var previousConsentInfo = content_module_state.consentInfo; if (previousConsentInfo === undefined){ previousConsentInfo = {}; } var consentInfoString = JSON.stringify(previousConsentInfo); var consentInfo64 = pk.base64url.encode(consentInfoString); var natural = pk.ptools.getPersona('kind', 'natural'); var options = natural.data.options; var identifier_class = options.startup_identifier ? 'clms_1' : 'clms_0'; var pin_class = options.startup_pin ? 'clms_1' : 'clms_0'; if (client_name_warning){ client_name_warning_class = 'clms_1'; } // var cardset_blurb = 'Create and use persona cards to control what kind of data you share with different websites &nbsp;'; var cardset_blurb = 'Select an ID'; var contentModuleState64 = pk.base64url.encode(JSON.stringify(content_module_state)); res.render(viewPath + 'get_consent', { title: 'Getting Consent', client_name: client_name, client_uri: client_uri, client_redirect_uri: client_info.redirect_uri, cardset_blurb: cardset_blurb, company_logo: company_logo, danger: client_name_warning_class, client_info_class: client_info_class, identifier_class: identifier_class, pin_class: pin_class, scope_info: scopeInfo64, consent_info: consentInfo64, content_module_state: contentModuleState64, encoded_sts_state_bundle: encoded_sts_state_bundle }); } function setConsentMode(mode) { if (mode){ var cmsString = document.getElementById("content_module_state").value; var content_module_state = JSON.parse(pk.base64url.decode(cmsString)); if (content_module_state.login_hint){ var login_persona = pk.ptools.getPersona("id", content_module_state.login_hint); if (login_persona){ if (login_persona.kind === 'credential'){ pk.ptools.output_card(true); return; } } } } pk.util.setElementVisibility('new_consent', !mode); pk.util.setElementVisibility('existing_consent', mode); } async function processConsentUserAgentResponse (pk, req, res){ pk.util.log_debug('--- WALLET: PROCESS CONSENT USER AGENT RESPONSE ---'); var params = req.body; pk.util.log_detail('params', params); // TODO: check with server and ensure encryption var natural = pk.ptools.getPersona('kind', 'natural'); var options = natural.data.options; if (options.startup_pin){ if (options.startup_pin !== params.holder_credential){ pk.pmanager.managerNotification('Invalid Wallet PIN', 'alert-danger', true); return; } } var content_module_state = JSON.parse(pk.base64url.decode(params.content_module_state)); if (params.error){ content_module_state.error = params.error; } else{ content_module_state.scope_claim_map = params.scope_claim_map; var consentInfoString = pk.base64url.decode(params.scope_info); var consentInfo = JSON.parse(consentInfoString); // add creds to consent scopeArray since creds behave like scopes for (var i=0; i<params.scopes_and_creds.length; i++){ var scred = params.scopes_and_creds[i]; if (consentInfo.scopeArray.includes(scred)){ continue; } consentInfo.scopeArray.push(scred); } if (params.accepted === false){ var index = consentInfo.scopeArray.indexOf('openid'); if (index > -1) { consentInfo.scopeArray.splice(index, 1); } } if (params.currentPersona !== undefined){ consentInfo.currentPersona = params.currentPersona; } if (params.credentialIssuerId !== undefined){ consentInfo.credentialIssuerId = params.credentialIssuerId; } // TODO: make it possible to use the same thum that will be used in the final token // with persona keys, that could be the thumbprint of the persona key. // with pairwise keys, it is the thumbprint of the pairwise key params.claims.sub = consentInfo.currentPersona; content_module_state.newIdTokenContent = params.claims; content_module_state.consentInfo = consentInfo; } pk.sts.applyConsentResponse(res, params.encoded_sts_state_bundle, content_module_state); } function processAjaxRequest (pk, req, res){ pk.util.log_debug('--- WALLET: PROCESS AJAX REQUEST ---'); var params = req.body; pk.util.log_detail('params', params); if (validateStartupCredentials(res, params.startup_identifier, params.startup_pin) !== true){ return; } switch (params.op){ case 'check_credentials': sendFauxAjaxSubmit(res); break; default: sendFauxAjaxError(res, 'unknown_op', params.op); } } function generateUserinfo(res, params, consentInfo, accessTokenContent){ pk.util.log_debug('--- WALLET: GENERATE USERINFO ---'); pk.util.log_debug('Generating userInfo in wallet\r\n'); pk.util.log_detail('sub', accessTokenContent.sub); var account = getAccountBySub(accessTokenContent.sub); pk.sts.submitUserinfoResponse(res, moduleName, params, consentInfo, content_module_state.consentInfo.tokenContent); } /*********************************************************************************************/ // Simplistic stubs function validateStartupCredentials(res, startup_identifier, startup_pin){ var options = pk.ptools.getPersona('kind', 'natural').options; if (options.startup_identifier){ if (!startup_identifier){ return sendFauxAjaxError(res, 'invalid_authorization_parameters'); } startup_identifier = startup_identifier.toLowerCase(); if (options.startup_identifier.toLowerCase() !== startup_identifier){ return sendFauxAjaxError(res, 'incorrect startup id'); } } if (options.startup_pin){ if (!startup_pin){ return sendFauxAjaxError(res, 'invalid_authorization_parameters'); } if (options.startup_pin !== startup_pin){ pk.util.log_detail('startup id entered', startup_identifier); pk.util.log_detail('startup pin entered', startup_pin); return sendFauxAjaxError(res, 'invalid_pin'); } } return true; } function getAccountBySub(sub){ for(var key in userAccounts){ if (userAccounts[key].sub === sub){ return userAccounts[key]; } } } /*********************************************************************************************/ // utility function sendFauxAjaxError(res, error, error_description){ pk.util.log_detail('SENDING AJAX ERROR', error); var payload = { kind: 'error', detail: { error: error, error_description: error_description } }; res.json(payload); return false; } function sendFauxAjaxSubmit(res){ var payload = { kind: 'submit', }; res.json(payload); return true; } async function token_presentation_options(params){ switch (params.option){ case 'share_consented_claims': var config = pk.ptools.getPersona('natural').data.options; return config['share_consented_claims']; case 'tokenSigningKey': return await getPersonaSigningKey(params); break; case 'imposeFormPostResponseMode': case 'vcFormat': if (params.value){ token_presentation_values[params.option] = params.value; } else{ return token_presentation_values[params.option]; } break; } } async function getPersonaSigningKey(params){ try{ var sub = params.sub; if (sub === undefined){ throw('tokenSigningKey requested but no sub specified'); } // use the did index to look up the key with the correct did var keyAndStore = await pk.key_management.loadSingleKey({ dictionary: {"did": sub } }); return keyAndStore.keyObject; } catch(err){ throw(err); } } function process_pickup_uri(pk, req, res){ var pickup_uri = req.query.pickup_uri; if (!pickup_uri){ res.statusCode = 400; res.end(); return; } var credential_pickup_def = pk.util.operator_profile.wallet_config_group.credential_pickup; var matching_issuer = ''; if (credential_pickup_def){ for (var key in credential_pickup_def){ if (pickup_uri.startsWith(key)){ matching_issuer = credential_pickup_def[key]; break; } } } if (!matching_issuer){ res.statusCode = 400; res.end(); return; } // handle bug in OH rquest var pickup_uri_offset = req.originalUrl.indexOf('pickup_uri='); pickup_uri = req.originalUrl.substr(pickup_uri_offset + 11); pickup_uri = encodeURIComponent(pickup_uri); var wallet_url = pk.sts.selfIssuedIssuerIdentifier(); var claimsObj = { "id_token":{ "shc":{ value: pickup_uri } } } var claims = JSON.stringify(claimsObj); var url = wallet_url + '?req_cred=' + matching_issuer + '&claims=' + claims + '&next_step=' + wallet_url + '%3Fpage%3Dpersonas'; window.location = url; } async function process_wallet_entry_point(pk, req, res){ if (req.query.iss && req.query.login_hint){ return process_oidc_initiate_login(pk, req, res); } } async function process_oidc_initiate_login(pk, req, res){ try{ var error_message = 'Error getting oidc_initiate_login params'; var iss = req.query.iss; var login_hint = req.query.login_hint; var requiredProperties = { iss: iss } // check existience of credential var existing_credential; var registerResult; // var cred_persona = pk.ptools.getPersona('target', requiredProperties); if (!existing_credential){ var error_message = 'Error getting credential issuer metadata'; var metadataString = await pk.token.requestOPMetadata(iss); error_message = 'Unable to parse credential_issuer metadata'; var credential_issuer_metadata = JSON.parse(metadataString); var registration_endpoint = credential_issuer_metadata.registration_endpoint; if (!registration_endpoint){ error_message = 'Unable to retrieve credential_issuer registration endpoint'; throw("No registration endpoint"); } var postData = { redirect_uris: [ pk.sts.selfIssuedIssuerIdentifier() ] } var http_options = { url: registration_endpoint, method: 'POST', parseJsonResponse: true, headers: [ { name: 'Accept', value: 'application/json' }, { name: 'Content-type', value: 'application/json' } ], postData: postData } registerResult = await pk.util.jsonHttpData(http_options); registerResult.pkce = await pk.simple_crypto.createB64Code(48); } var persona_id = await pk.ptools.locate_or_add_credential_persona(iss, registerResult); if (!persona_id){ throw ('no persona_id located or added in create_persona_with_credential'); } // await request_credential(persona_id, login_hint, iss, next_step, claims) await request_credential(persona_id, login_hint, iss); } catch(err){ console.log('ERROR in oidc_initiate_login - ' + error_message, err); } } async function request_credential(persona_id, login_hint, issuer, next_step, claims) { try { error_message = 'Unable to connect to the credential_issuer'; var metadataString = await pk.token.requestOPMetadata(issuer); error_message = 'Unable to parse credential_issuer metadata'; var credential_issuer_metadata = JSON.parse(metadataString); error_message = 'Error getting or creating credential_issuer claims'; var credential_issuer_claims = await getOrCreateCredentialIssuerClaims(issuer, persona_id); if (credential_issuer_claims){ var endpoint = credential_issuer_metadata.authorization_endpoint; var serviceUrl = pk.sts.selfIssuedIssuerIdentifier(); var redirect_uri = serviceUrl; var nonce = await pk.simple_crypto.randomString(); var nonceInfo = { nonce: nonce, persona_id: persona_id } pk.nonceCache.set('cred_request_nonce_info', JSON.stringify(nonceInfo)); var scope = 'openid openid_credential'; var response_type = 'id_token token'; response_type = 'code'; var state_params = { tok_ept: credential_issuer_metadata.token_endpoint, cred_ept: credential_issuer_metadata.credential_endpoint, sub: persona_id } if (next_step){ state_params.next_step = next_step; } var state = pk.util.createParameterString(state_params).substring(1); var persona = pk.ptools.getPersona('id', persona_id); var parameters = { client_id: persona.client_id, redirect_uri: redirect_uri, response_type: response_type, nonce: nonce, state: state, scope: scope, login_hint: login_hint, code_challenge: await pk.simple_crypto.digestSha256(persona.data.pkce), code_challenge_method: 'S256' }; if (claims){ parameters.claims = claims; } var parameterString = pk.util.createParameterString(parameters); window.location = endpoint + parameterString; } else{ alert('Error populating credential_issuer object'); throw ("no credential_issuer claims in request_credential"); } } catch (err){ var alert_message = error_message + err; if (error_message){ pk.pmanager.managerNotification(error_message, 'alert-warning', true); } else{ pk.pmanager.managerNotification(removeNotification); } } async function getOrCreateCredentialIssuerClaims(issuer, persona_id){ try { var credential_issuer_claims_id = persona_id; var credential_issuer_claims = await pk.dbs['wallet'].provider.getDocument(pk.dbs['wallet'], 'credential_issuer_claims', credential_issuer_claims_id); } catch (err){ // this doesn't exist yet - create it so the response from the // credential_issuer will be accepted var proposed_credential_issuer_claims = { issuer: issuer, id: persona_id }; credential_issuer_claims = await pk.dbs['wallet'].provider.createOrUpdateDocument(pk.dbs['wallet'], 'credential_issuer_claims', proposed_credential_issuer_claims); } return credential_issuer_claims; } } },{}],4:[function(require,module,exports){ var startupInvoked = false; var mainReadyToStart = false; pk.util.log_debug('[Main] Registering Service Worker'); navigator.serviceWorker.register('/wallet/service-worker.js', {scope: '/wallet/'}) .then(reg => { // reg is a serviceWorkerRegistration object pk.util.log_debug('[Main] Service Worker Registration Succeeded'); // Handler for messages coming from the service worker if (!startupInvoked && mainReadyToStart){ // pk.util.log_debug("Beginning Startup with delayed Registration"); // beginStartup(); pk.util.log_debug("NOT beginning Startup due to delayed Registration"); } }, err => { pk.util.log_debug('[Main] Service Worker Registration failed', err); }); navigator.serviceWorker.addEventListener('message', function(event){ var action = event.data.action; switch(action){ case 'install': if (startupInvoked){ pk.util.log_debug('[Main] Ignoring INSTALL message from ServiceWorker because startup begun.'); } else{ pk.util.log_debug('[Main] Received INSTALL message from Service Worker and invoking startup.') beginStartup(); } break; case 'managerNotification': case 'masterNotification': var retVal = masterNotification(event.data); break; } }); function send_message_to_serviceWorker(msg){ var safeMsg = pk.base64url.encode(JSON.stringify(msg)); return new Promise(function(resolve, reject){ if (navigator.serviceWorker.controller === null){ reject('serviceworker-not-ready'); } else{ // Create a Message Channel var msg_chan = new MessageChannel(); // Handler for recieving message reply from service worker msg_chan.port1.onmessage = function(event){ if(event.data.error){ reject(event.data.error); }else{ resolve(JSON.parse(pk.base64url.decode(event.data))); } }; // Send message to service worker along with port for reply navigator.serviceWorker.controller.postMessage(safeMsg, [msg_chan.port2]); } }); } // external packagefs paintLoadScreen(); var frameworkModules = { key_management: pk.key_management, did_management: pk.did_management, sts: pk.sts, token: pk.token, pmanager: pk.pmanager, pexchange: pk.pexchange, serialize64: pk.serialize64, claimer_crypto: pk.claimer_crypto, simple_crypto: pk.simple_crypto } var dbs = {}; var dbScaffold = pk.util.createDbScaffold(); var jsonForm = require('../wallet/jsonForm'); var featureModules = { 'wallet': { code: require('../wallet/wallet'), response_types: pk.util.config.content_modules['wallet']['responseTypes'], resources: { scope_claim_map: require('../wallet/data/scope_claim_map_auto') } }, 'sts': { resources: { scope_claim_map: require('../wallet/data/scope_claim_map_auto') } } }; for (var contentModuleName in pk.util.config.content_modules){ pk.app.registerCookie(pk.sts.cookie_identifier + contentModuleName); } if (pk.util.config.sts.httpsServerUrl !== undefined){ var httpsServerUrl = pk.util.url(pk.util.config.sts.httpsServerUrl); } var cookieKeys = ["abcdef123", "defhij234"]; var pinResolvers = {}; pk.feature_modules = featureModules; pk.dbs = dbs; pk.util.content_module_signing_key = pk.key_management.contentModuleSigningKey; pk.util.cookieKeys = cookieKeys; pk.util.httpsServerUrl = httpsServerUrl; pk.util.jsonForm = jsonForm; pk.util.masterNotification = masterNotification; pk.util.send_message_to_serviceWorker = send_message_to_serviceWorker; pk.util.swMessages = {}; pk.util.sync_info = new sync_info(); pk.util.sync_info.load_sync_time(); pk.util.submitPin = submitPin; // pk.util.get_c_key = get_c_key; function masterNotification(msg){ if (msg.action === 'managerNotification'){ elementName = 'manager_notifications' } else{ elementName = 'notification'; } var message = msg.message; var className = msg.className; var dismissible = msg.dismissible; var notificationsDiv = document.getElementById(elementName); if (!notificationsDiv){ notificationsDiv = document.getElementById('notification'); } if (notificationsDiv){ if (typeof message === "object"){ if (message.stack){ message = message.stack; } } else if (typeof message === "string"){ if (className === undefined){ className = 'alert-success'; } var notificationDiv = document.createElement('div'); notificationDiv.classList.add('alert', className); if (dismissible){ notificationDiv.classList.add('alert-dismissible'); } var html = ''; if (dismissible){ html += '<button type="button" class="close" data-dismiss="alert">&times;</button>'; } html += message; notificationDiv.innerHTML = html; notificationsDiv.appendChild(notificationDiv); return notificationDiv; } else if (message === false || message === undefined){ notificationsDiv.innerHTML = ''; } else if (message.parentElement){ message.parentElement.removeChild(message); } } } mainReadyToStart = true; if (navigator.serviceWorker.controller === null){ pk.util.log_debug('[Main] Controller not yet present.'); } else{ pk.util.log_debug('[Main] Controller exists.'); beginStartup(); } async function beginStartup(){ if (startupInvoked){ return; } else{ startupInvoked = true; } var potentialErrorMessage = ''; try { pk.util.operator_profile = await getOperatorProfile(); pk.app.entryPointUrlFunction = entryPointUrlFunction; if (!('indexedDB' in window)) { throw ('This browser doesn\'t support IndexedDB'); } potentialErrorMessage = "Error initializing databases"; dbResult = await initializeAllDataBases(dbScaffold); for (var dbInfo in pk.dbs){ if (dbInfo.contentModuleName === 'sts'){ continue; } else{ dbInfo.flockMembership = pk.dbs['sts'].flockMembership; } } for (var module_name in pk.feature_modules){ try { if (pk.feature_modules[module_name].code){ pk.feature_modules[module_name].code.registerEndpoints(pk); } } catch(err){ if (claimer_config.logging > 1){ console.error(chalk.red('Unable to register Endpoints for content module ' + module_name)); console.error(err); } else{ console.error(chalk.red('Unable to register Endpoints for content module ' + module_name)); console.error(chalk.red(err.message)); } } } for (var fmod in frameworkModules){ frameworkModules[fmod].registerEndpoints(pk); } console.log("checkIncognito"); potentialErrorMessage = "Error checking incognito"; var incognito = await checkIncognito(); console.log("queryPwaServerForStartupMode"); var startupMode = await queryPwaServerForStartupMode(); console.log("dbScaffold wallet"); if (dbScaffold['wallet']){ var c_key_buffer; try{ var w_key_string = await send_message_to_serviceWorker({action: 'get_w_key'}); if (w_key_string && w_key_string.length > 0){ var w_key = forge.util.createBuffer(pk.base64url.decode(w_key_string)); var dbPwaInfoArray = await pk.dbs['wallet'].provider.queryCollection (pk.dbs['wallet'], 'pwa', {}); var dbPwaInfo = dbPwaInfoArray[0]; var c_encryptedObj = JSON.parse(dbPwaInfo.c_encrypted); c_key_buffer = pk.pmanager.wallet_decrypt('AES-CBC', w_key, c_encryptedObj); } if (c_key_buffer){ for (var key in dbScaffold){ if (pk.util.operator_profile.wallet_config_group.encrypted === false){ pk.dbs[key].c_key = false; } else{ pk.dbs[key].c_key = c_key_buffer; console.log('c_key set in dbs[' + key + ']'); } } } else{ throw('Pwa unable to start up.') } } catch (err){ pk.util.log_error('queryPwaServerForStartupMode: Pwa unable to start up.', err); throw('c_key error - startup not possible.'); } } potentialErrorMessage = "Error initializing Personas"; await pk.ptools.initializePersonas(); var allPersonas = await pk.ptools.loadPersonas(incognito); potentialErrorMessage = "Error loading or creating keys"; await pk.key_management.loadOrCreateKeys(); console.log('invokeEntryPoint'); pk.app.invokeEntryPoint(); // if there have been changes to the database // publish encrypted to the service console.log('pexchange.exchange temporarily disabled'); // var result = await pk.pexchange.exchange(); // TODO: improve alg to receive notification when changes // are made and dispatch updates accordingly // var timerHandle = setInterval(exchangeTimer, 30000); // async function exchangeTimer() { // var result = await pk.pexchange.exchange(); // } var version = await send_message_to_serviceWorker({action: 'version'}); if (version){ pk.util.claimer_version = version; payload = { action: 'serviceUrl', serviceUrl: window.location.origin } var serviceUrlSet = send_message_to_serviceWorker(payload); } if (serviceUrlSet){ return(true); } } catch (err){ pk.util.log_debug('[Main] *******************************'); pk.util.log_debug(potentialErrorMessage + err); pk.util.log_debug('[Main] *******************************'); } } /* async function get_c_key(dbPwaInfo){ var w_key_string = await send_message_to_serviceWorker({action: 'get_w_key'}); var w_key = forge.util.createBuffer(pk.base64url.decode(w_key_string)); var auth_required = true; var c_key; while (auth_required){ var parameters = {}; try { if (w_key_string && w_key_string.length > 0){ c_key = pk.pmanager.wallet_decrypt('AES-CBC', w_key, dbPwaInfo.c_encrypted); } else{ var pinInfo = await getPin(last_error); var pin = pinInfo.pin; var pii = pinInfo.pii; if (pin.length < wallet_startup.pin_length){ error = 'Pin must be at least ' + wallet_startup.pin_length + ' characters long'; throw(error); } var md = forge.md.sha256.create(); md.update(pin + pii); var w_key = md.digest(); if (w_key !== dbPwaInfo.w_key){ error = 'Invalid Pin'; throw (error); } c_key = pk.pmanager.wallet_decrypt('AES-CBC', w_key, dbPwaInfo.c_encrypted); } } catch(err){ last_error = err; } auth_required = false; } return c_key; } */ async function queryPwaServerForStartupMode(){ var wallet_startup = pk.util.operator_profile.wallet_config_group.wallet_startup; var result = await pk.util.queryPwaApi('/wallet/wallet_id_request', {}); // if no connectivity if (!result){ try{ dbPwaInfo = await pk.dbs['wallet'].provider.queryCollection (pk.dbs['wallet'], 'pwa', {}); existing_dbPwaRecord = dbPwaInfo; } catch(err){ if (err.code === 404){ pk.util.log_error('queryPwaServerForStartupMode', err); throw('Pwa unable to start up.') } } } else{ var wallet_id = result.wallet_id; var dbPwaInfo; var existing_dbPwaRecord; var c_encrypted; try{ dbPwaInfo = await pk.dbs['wallet'].provider.getDocument(pk.dbs['wallet'], 'pwa', result.wallet_id); existing_dbPwaRecord = dbPwaInfo; } catch(err){ if (err.code !== 404){ pk.util.log_error('queryPwaServerForStartupMode', err); } } } var w_key_string = await send_message_to_serviceWorker({action: 'get_w_key'}); var auth_required = true; var last_error = ''; while (auth_required){ var parameters = {}; try { if (w_key_string && w_key_string.length > 0){ break; } var pinInfo = await getPin(last_error); var pin = pinInfo.pin; var pii = pinInfo.pii; if (pin.length < wallet_startup.pin_length){ error = 'Pin must be at least ' + wallet_startup.pin_length + ' characters long'; throw(error); } var md = forge.md.sha256.create(); md.update(pin + pii); var w_key = md.digest(); var serviceWorkerResponse = await send_message_to_serviceWorker( { action: 'set_w_key', data: pk.base64url.encode(w_key.bytes()) }); // w_id var w_id = await pk.simple_crypto.digestSha256( w_key.bytes() + wallet_id); if (dbPwaInfo){ parameters = { w_id: w_id } } else{ // c_key (content_key encrypted in c_encrypted) var c_key_buffer = forge.util.c