oidc-lib
Version:
A library for creating OIDC Service Providers
949 lines (826 loc) • 25.3 kB
JavaScript
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">×</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.createBuffer(forge.random.getBytesSync(16));
// w_key
// encrypt c_key using w_key
var c_encryptedObj = pk.pmanager.wallet_encrypt('AES-CBC', w_key, c_key_buffer);
var c_encrypted = JSON.stringify(c_encryptedObj);
parameters = {
w_id: w_id,
c_encrypted: c_encrypted
}
}
result = await pk.util.queryPwaApi('/wallet/register_request', parameters);
switch (result.status){
case 'ERROR':
pk.util.log_debug('queryPwaServerForStartupMode: ' + result.error);
throw (result.error);
case 'RESTORE':
await process_restore(result);
break;
case 'OK':
if (!dbPwaInfo){
dbPwaInfo = {};
}
dbPwaInfo.id = wallet_id;
dbPwaInfo.w_id = w_id;
dbPwaInfo.pii = pii;
if (c_encrypted){
dbPwaInfo.c_encrypted = c_encrypted;
}
if (dbPwaInfo.c_encrypted){
var c_encryptedObj = JSON.parse(dbPwaInfo.c_encrypted);
c_key_buffer = pk.pmanager.wallet_decrypt('AES-CBC', w_key, c_encryptedObj);
var serviceWorkerResponse = await send_message_to_serviceWorker(
{
action: 'set_c_key',
data: pk.base64url.encode(c_key_buffer.bytes())
});
}
if (isRecordChanged(dbPwaInfo, existing_dbPwaRecord)){
await pk.dbs['wallet'].provider.createOrUpdateDocument(pk.dbs['wallet'], 'pwa', dbPwaInfo);
}
// await pk.ptools.initializePersonas();
break;
default:
pk.util.log_error('queryPwaServerForStartupMode', 'unexpected status: ' + result.status);
throw('Unexpected status: ' + result.status);
}
auth_required = false;
}
catch(err){
last_error = err;
}
}
return result;
}
function isRecordChanged(new_record, old_record){
if (!old_record){
return true;
}
for (var key in new_record){
if (new_record[key] !== old_record[key]){
return true;
}
}
for (var key in old_record){
if (old_record[key] !== new_record[key]){
return true;
}
}
return false;
}
async function process_restore(api_result){
try{
if (!api_result.device_wallet){
alert('unexpected input in process_restore: ' + JSON.stringify(api_result));
return;
}
var device_wallet = api_result.device_wallet;
if (device_wallet.version !== '1.0'){
alert('Could not restore wallet version: ' + device_wallet.version);
return;
}
if (device_wallet.type !== 'fullSync'){
alert('Could not process wallet restore of type: ' + device_wallet.type);
return;
}
for (var dbName in device_wallet.databases){
var dbInfos = device_wallet.databases[dbName];
for (var collectionName in dbInfos){
var collectionRecords = dbInfos[collectionName];
for (var i=0; i < collectionRecords.length; i++){
var record = collectionRecords[i];
var fixup = await pk.key_management.keyObjectImportKey(dbName, collectionName, record);
var result = await pk.dbs[dbName].provider.createOrUpdateDocument(pk.dbs[dbName], collectionName,
fixup ? fixup : record);
}
}
}
}
catch(err){
alert('Error in process_restore: ' + err);
}
}
async function getPin(last_error){
return new Promise((resolve, reject) => {
var get_pin_b64 = pk.app.module_views.viewCollection['\\wallet\\views\\get_pin'];
var get_pin_html = pk.base64url.decode(get_pin_b64);
pinResolvers.resolve = resolve;
pinResolvers.reject = reject;
var notificationDiv = document.getElementById('render');
notificationDiv.innerHTML = get_pin_html;
if (last_error){
var pin_alert_divEl = document.getElementById('pin_alert_div');
pin_alert_divEl.innerHTML = last_error;
pk.util.setElementVisibility('pin_alert_div', true);
}
var getPinSaveEl = document.getElementById('get_pin_save');
getPinSaveEl.setAttribute('onclick', 'pk.util.submitPin()');
});
}
function submitPin(){
var pinEl = document.getElementById('pin');
var pin_value = pinEl.value;
var pinInfo = {
pin: pin_value,
pii: 'foobar'
}
var notificationDiv = document.getElementById('render');
notificationDiv.innerHTML = '';
pinResolvers.resolve(pinInfo);
}
async function initializeAllDataBases(dbScaffold){
var potentialErrorMessage = 'Error with initialize databases - ';
try{
for (var cmName in dbScaffold){
var provider = dbScaffold[cmName];
if (provider !== undefined){
var dbInfo = await provider.initialize(pk, cmName, provider);
pk.dbs[dbInfo.contentModuleName] = dbInfo;
}
}
}
catch (err){
throw(potentialErrorMessage + err);
}
}
function calculateContentModuleScopes(content_scope_claim_maps){
var contentModuleScopes = {};
for (var module_name in content_scope_claim_maps){
if (content_scope_claim_maps[module_name] !== null){
var moduleScopes = [];
var scope_claim_map = content_scope_claim_maps[module_name];
for (var key in scope_claim_map){
if (key.startsWith('fieldset_')){
var scope = scope_claim_map[key].scope;
if (scope !== undefined){
moduleScopes.push(scope);
}
}
}
contentModuleScopes[module_name] = moduleScopes;
}
}
return contentModuleScopes;
}
function entryPointUrlFunction(req){
pk.util.log_debug("**** REMOVE query.id_token from entryPointUrlFunction1! ****");
if (req.method === "GET"){
if (req.query.redirect_uri){
return '/wallet/auth';
}
else if (req.query.req_cred){
return '/wallet/req_cred';
}
else if (req.hash){
return('/wallet/process_credential_issuer_response');
}
else if(req.query.code){
return('/wallet/process_code');
}
else if (req.query.id_token){
return('/wallet/process_credential_issuer_response');
}
else if (req.query.page){
return('/wallet/manager');
}
else if (req.query.error){
return('/wallet/oauth_error');
}
else if (req.query.w_id){
alert('wallet configuration complete');
return('/wallet/manager');
}
else if (req.query.pickup_uri){
return('/wallet/pickup_uri');
}
else if (req.query['entry_point']){
return('/wallet/entry_point');
}
else{
var paramsPresent = false;
for(var param in req.query){
paramsPresent = true;
break;
}
if (paramsPresent){
alert("Invalid entry point: " + req.originalUrl);
}
else{
return('/wallet/manager');
}
}
}
else{
alert('post entrypoints not implemented');
}
}
function storageSupported(){
return new Promise((resolve, reject) => {
var fs = window.RequestFileSystem || window.webkitRequestFileSystem;
if (!fs) {
reject('check failed?');
}
else {
fs(window.TEMPORARY,
100,
alert('true'),
alert('false'));
}
});
}
function checkIncognito(){
return new Promise((resolve, reject) => {
var callback = function(result, param){
result(param);
}
var fs = window.RequestFileSystem || window.webkitRequestFileSystem;
if (!fs) {
// for now assume it is safari. Otherwise use reject('ENOENT');
var storage = window.sessionStorage;
try {
storage.setItem("safari_private", "false");
storage.removeItem("safari_private");
} catch (e) {
if (e.code === DOMException.QUOTA_EXCEEDED_ERR && storage.length === 0) {
resolve(true);
}
}
resolve(false);
} else {
fs(window.TEMPORARY,
100,
callback.bind(undefined, resolve, false),
callback.bind(undefined, resolve, true)
);
}
});
}
function paintLoadScreen(){
var html = '\
<div class="center-block" style="padding: 20%">\
<img style="height: 100%; width: 100%; object-fit: contain" src=\'/wallet/images/icons/{{splash}}\'/>\
</div>';
/*
if (!window.location.search && (!pk || !pk.util ||
!pk.util.hubInstance || pk.util.hubInstance.flockDid === FLOCKNOTPRESENT)){
var renderEl = document.getElementById('render');
renderEl.innerHTML = html;
}
*/
var splash;
if (window.location.search.startsWith('?req_cred=')){
splash = 'blue-setup.png';
}
else if (!window.location.search){
splash = 'blue-512x512.png';
}
if (splash){
html = html.replace("{{splash}}", splash);
var renderEl = document.getElementById('render');
renderEl.innerHTML = html;
}
}
function sync_info(){
const LASTSYNCMONIKER = 'hub-last-sync';
var _last_sync_time = 0;
// get does not load, and set does a lazy save
Object.defineProperty(this, "last_sync_time", {
get: function() {
return _last_sync_time;
},
set: function(value) {
_last_sync_time = value;
localStorage.setItem(LASTSYNCMONIKER, value);
},
enumerable: true
});
// returns a promise
Object.defineProperty(this, "remove_sync_time", {
value: function() {
return new Promise((resolve, reject) => {
_last_sync_time = 0;
localStorage.remove(LASTSYNCMONIKER)
.then(result => {
if (result){
resolve();
}
}, err => {
reject(err);
})
});
}
});
// returns a promise
Object.defineProperty(this, "load_sync_time", {
value: function() {
return new Promise((resolve, reject) => {
var result = localStorage.getItem(LASTSYNCMONIKER);
_last_sync_time = result;
resolve(result);
});
}
});
}
function dataStore(){
var _ds_info = new pk.util.db_module('indexed_db')();
var _dbInfo = null;
Object.defineProperty(this, "set", {
value: function(name, value) {
return new Promise((resolve, reject) => {
var initPromise;
if (_dbInfo === null){
initPromise = _ds_info.initialize_local_storage(pk, 'sts', _ds_info);
}
else{
initPromise = Promise.resolve(_dbInfo);
}
initPromise
.then(dbInfo => {
if (dbInfo){
if (!_dbInfo){
_dbInfo = dbInfo;
}
var payload = {
id: name,
value: value
}
return _ds_info.createOrUpdateDocument(dbInfo, 'local_storage', payload);
}
}, err => {
reject(err);
})
.then(result => {
if (result){
resolve(result);
}
}, err => {
reject(err);
})
})
}
})
Object.defineProperty(this, "get", {
value: function(name, encoding) {
return new Promise((resolve, reject) => {
var initPromise;
if (_dbInfo === null){
initPromise = _ds_info.initialize_local_storage(pk, 'sts', _ds_info);
}
else{
initPromise = Promise.resolve(_dbInfo);
}
initPromise
.then(dbInfo => {
if (dbInfo){
if (!_dbInfo){
_dbInfo = dbInfo;
}
return _ds_info.getDocument(dbInfo, 'local_storage', name);
}
}, err => {
reject(err);
})
.then(payload => {
if (payload){
resolve(payload.value);
}
}, err => {
if (err && err.code === 404){
resolve(undefined);
}
else{
reject(err);
}
})
})
}
})
Object.defineProperty(this, "remove", {
value: function(name, encoding) {
return new Promise((resolve, reject) => {
var initPromise;
if (_dbInfo === null){
initPromise = _ds_info.initialize_local_storage(pk, 'sts', _ds_info);
}
else{
initPromise = Promise.resolve(_dbInfo);
}
initPromise
.then(dbInfo => {
if (dbInfo){
if (!_dbInfo){
_dbInfo = dbInfo;
}
return _ds_info.deleteDocument(dbInfo, 'local_storage', name);
}
}, err => {
reject(err);
})
.then(payload => {
if (payload){
resolve(payload);
}
}, err => {
reject(err);
})
})
}
})
}
async function getOperatorProfile(){
var profile_url = window.location.origin + '/wallet/operator_profile.json';
var options = {
url: profile_url,
method: 'GET',
headers: [ { name: 'Accept', value: 'application/json' } ]
};
var profile_content = {};
try{
var contentString = await pk.util.jsonHttpData(options);
profile_content = JSON.parse(contentString);
}
catch{
}
return profile_content;
}