UNPKG

oidc-lib

Version:

A library for creating OIDC Service Providers

1,436 lines (1,228 loc) 45.8 kB
module.exports = { registerEndpoints: registerEndpoints, dispatchManager: dispatchManager, manager: manager, oauth_error: oauth_error, create_persona_with_credential: create_persona_with_credential, delete_persona: delete_persona, dispatchOptions: dispatchOptions, optionsAddToHomeScreen: optionsAddToHomeScreen, optionsAddDevice: optionsAddDevice, optionsExchange: optionsExchange, optionsRemoveWallet: optionsRemoveWallet, executeDeviceAddition: executeDeviceAddition, conveyFlockMembershipCipher: conveyFlockMembershipCipher, process_credential_issuer_response: process_credential_issuer_response, managerNotification: managerNotification, pairwiseKeyConfig: pairwiseKeyConfig, didMethodConfig: didMethodConfig, wallet_encrypt: wallet_encrypt, wallet_decrypt: wallet_decrypt } // redirect fails when sending a fragment if debugger is running // this allows debugging of flow by stopping it so you can attach // the debugger after the redirect succeeds. const debugging_credential_issuer = false; const use_code_flow = true; // const NEW_ISSUER_REGISTRATION_RESPONSE_ENDPOINT = '/wallet/endpoints/issuer_registration.html'; const GENERATE_DID_MONIKER = "generate@"; // var shakespeare = require('./shakespeare'); const moduleName = 'wallet'; var g_viewPath; var g_newDeviceInfo = {}; var g_membershipExchangeKey; function registerEndpoints() { g_viewPath = pk.feature_modules['wallet'].code.VIEWPATH; pk.app.post('/wallet/options_response', function(req, res){ processOptionsResponse(pk, req, res); }); /* ** TBD - Remove when we verify no longer used pk.app.post('/wallet/complete_add_card_credential', function(req, res){ completeAddCardCredential(pk, req, res); }); pk.app.get('/wallet/invoke_credential_issuer', function(req, res){ alert("invoke_credential_issuer from walletManager"); // note currently this won't work without fixing the request parameter in fucntion invoke_credential_issuer(req, res); }); */ pk.app.get('/wallet/process_credential_issuer_response', function(req, res){ process_credential_issuer_response(req, res); }); pk.app.get('/wallet/process_code', async function(req, res){ await process_code(req, res); }); pk.app.get('/wallet/req_cred', function(req, res){ process_req_cred(req, res); }); } function dispatchManager(page){ if (page !== 'scan'){ pk.pscan.stop(); } pk.app.renderDispatcher('get', '/wallet/manager', { page: page }); } async function manager(pk, req, res){ var page = req.query['page']; var notification = req.query['notification']; var functions = { first_use: managerFirstUse, scan: scan, personas: managerDataCards, credential_issuers: managerCredentialIssuers, relationships: managerRelationships, options: managerOptions }; var options = await getManagerOptions(); if (page === undefined){ // if (options.first_use){ // page = 'first_use'; // } if (pk.ptools.getCredentialCount() === 0){ page = 'personas'; } else{ page = 'scan'; } } else if (page !== 'personas'){ // need to check if dirty from changes before doing something this // draconian // pk.ptools.savePersonaChanges(); } if (notification){ pk.ptools.reloadPersonas(); managerNotification(notification, 'alert-warning', true); } var func = functions[page]; if (func === undefined){ res.render(g_viewPath + 'error_received', { component: 'DID Manager', error: 'invalid page parameter', error_description: 'value received: ' + page }); return; } func(pk, req,res); } function managerDataCards(pk, req, res){ var claims = { id_token: {}, userinfo: {} }; // var scopeArray = ['openid', 'email', 'phone', 'address', 'profile']; var scopeArray = []; var scopeInfo = { claims: claims, scopeArray: scopeArray }; var currentPersona; if (req.query.selected){ currentPersona = req.query.selected; } else{ currentPersona = pk.ptools.getPersona('selected').id; } if (pk.ptools.getCredentialCount() === 0){ var msg = 'You currently have no credentials. Click on the <b>+</b> to access some recommendations, or connect with a Credential Issuer using your browser.' managerNotification(msg, 'alert-warning', true); } if (req.query.msg){ managerNotification(req.query.msg, 'alert-warning', true); } var consentInfo = { claims: claims, currentPersona: currentPersona, scopeArray: scopeArray, tokenContent: {} }; var content_module_state = { client_id: null, consentInfo: consentInfo, scopeInfo: scopeInfo }; var scopeInfoString = JSON.stringify(scopeInfo); var scopeInfo64 = pk.base64url.encode(scopeInfoString); var consentInfoString = JSON.stringify(consentInfo); var consentInfo64 = pk.base64url.encode(consentInfoString); var contentModuleStateString = JSON.stringify({}); var contentModuleState64 = pk.base64url.encode(contentModuleStateString); res.render(g_viewPath + 'did_manager', { scope_info: scopeInfo64, consent_info: consentInfo64, content_module_state: contentModuleState64, current_manager_page: 'personas_page', pageHeading: 'My credentials', encoded_sts_state_bundle: '' }); } async function managerCredentialIssuers(pk, req, res){ var dbInfo = pk.dbs['wallet']; var vInfos = await createCredentialIssuerList('claim'); res.render(g_viewPath + 'did_manager', { title: 'Web Sites', current_manager_page: 'manager_credential_issuers', pageHeading: 'My credential issuers', credentialIssuerArray: vInfos }); } async function createCredentialIssuerList(sortBy){ try { var dbInfo = pk.dbs['wallet']; var vInfos = []; var credential_issuers = await dbInfo.provider.queryCollection(dbInfo, 'credential_issuer_claims', {}); for (var vcount=0; vcount < credential_issuers.length; vcount++){ var credential_issuer = credential_issuers[vcount]; if (credential_issuer.vc_constants){ var description = credential_issuer.vc_constants.description; var typeArray = credential_issuer.vc_constants.type; var credential_type; for (var i=0; i< typeArray.length; i++){ var type = typeArray[i]; if (type === 'VerifiableCredential'){ continue; } credential_type = type; } if (!credential_type){ continue; } } var cookieArray = await issuer_get_cookies(credential_type); var vInfo = { credential_type: credential_type, issuer: credential_issuer.issuer_name, description: description }; vInfos.push(vInfo); } if (sortBy !== undefined){ vInfos = vInfos.sort(function(a, b){ a = a[sortBy]; b = b[sortBy]; if (a > b){ return 1; } else if (a === b){ return 0; } else{ return -1; } }); } return (vInfos); } catch (err){ }; } async function oauth_error(pk, req, res){ alert('oauth_error called'); var nonce = req.query.nonce; var nonceInfo; var nonceInfoStr = pk.nonceCache.get('cred_request_nonce_info'); if (nonceInfoStr){ nonceInfo = JSON.parse(nonceInfoStr); } if (!nonceInfo || nonce !== nonceInfo.nonce){ alert('invalid nonce in oauth_error'); return; } var page; var notification; switch (req.query.error.toLowerCase()){ case 'access_denied': if (req.query.error_description.toLowerCase().startsWith('credential_revoked:')){ delete_persona(nonceInfo.persona_id); notification = req.query.error_description; var refresh_url = 'https://' + window.location.host + window.location.pathname + '?page=personas&notification=' + notification; window.location = refresh_url; return; } break; default: break; } pk.app.renderDispatcher('get', '/wallet/manager', { page: page, notification: notification }); } async function delete_persona(credential_id){ var dbInfo = pk.dbs['wallet']; await dbInfo.provider.deleteDocument(dbInfo, 'accounts', credential_id, null); dbInfo = pk.dbs['sts']; await dbInfo.provider.deleteDocument(dbInfo, 'keys', credential_id, null); } function issuer_get_cookies(credential){ return new Promise((resolve, reject) => { var credential_root = credential.substr(0, credential.lastIndexOf('/')); var iframeUrl = credential_root + "/cookies" ; var launchIframeString = '\ <div style="display: none;">\ <iframe id="wallet_iframe"\ width="300"\ height="200"\ src="' + iframeUrl + '">\ </iframe>\ </div>'; var timer = setTimeout(function () { reject('smart credential offline'); }, 10000); var listener = window.addEventListener("message", (event) => { clearTimeout(timer); resolve(event.data); }, false); var cookieEl = document.getElementById('issuer_get_cookies'); if (cookieEl){ cookieEl.innerHTML = launchIframeString; } else{ var cookieEl = document.createElement('div'); cookieEl.setAttribute('id', 'issuer_get_cookies'); cookieEl.innerHTML = launchIframeString; document.body.append(cookieEl); } }); } function scan(pk, req, res){ res.render(g_viewPath + 'manager_scan', { title: 'Proof Request Scanner', pageHeading: 'Scan a cred request...' }); } function managerRelationships(pk, req, res){ var stsDbInfo = pk.dbs['sts']; // using an object to obtain unique between array names var websites = {}; stsDbInfo.provider.queryCollection(stsDbInfo, 'clients', {}) .then(function(clients){ for (var i=0; i<clients.length; i++){ websites[clients[i].client_name] = { "url": clients[i].client_uri, "note": 'Issued' } } walletDbInfo = pk.dbs['wallet']; return walletDbInfo.provider.queryCollection(walletDbInfo, 'websites', {}); }, function(err){ reject('unable to query clients in manageRelationships'); }) .then(function(apps){ for (var i=0; i<apps.length; i++){ if (websites[apps[i].id]){ continue; } websites[apps[i].id] = { "url": apps[i].url, "note": 'R' }; } var websitesArray = []; var i = 0; for (var key in websites){ websitesArray[i++] = { name: key, url: websites[key]['url'], note: websites[key]['note'] } } res.render(g_viewPath + 'did_manager', { title: 'Web Sites', current_manager_page: 'manager_relationships', pageHeading: 'Web Sites', websitesArray: websitesArray }); }, function(err){ reject('unable to query apps in manageRelationships'); }); } function managerFirstUse(pk, req, res){ if (deferredPwaPrompt){ deferredPwaPrompt.prompt(); } res.render(g_viewPath + 'did_manager', { title: 'Web Sites', current_manager_page: 'manager_firstUse', pageHeading: 'First Use', }); } async function getManagerOptions(){ var naturalPersona = pk.ptools.getPersona('natural'); var options = naturalPersona.data.options; if (options.first_use === undefined){ options = pk.util.operator_profile.wallet_config_group.initial_options; setManagerOptions(options); } /* else{ for(var k in optionsTemplate){ if (options[k] === undefined){ options[k] = optionsTemplate[k]; } } } */ return options; } async function setManagerOptions(options){ var naturalPersona = pk.ptools.getPersona('natural'); naturalPersona.data.options = options; var dbInfo = pk.dbs['wallet']; await dbInfo.provider.createOrUpdateDocument(dbInfo, 'accounts', naturalPersona, null); } async function managerOptions(pk, req, res){ var options = await getManagerOptions(); var share_consented_claims_html = ''; if (options.share_consented_claims === true){ share_consented_claims_html = 'checked=true'; } var self_asserted_personas_html = ''; if (options.self_asserted_personas === true){ self_asserted_personas_html = 'checked=true'; } var chapi_wallet_html = ''; if (options.chapi_wallet === true){ chapi_wallet_html = 'checked=true'; } var auto_sync_html = ''; if (options.auto_sync === true){ auto_sync_html = 'checked=true'; } var dbInfo = pk.dbs['sts']; var retrievedClients = await dbInfo.provider.queryCollection( dbInfo, 'clients', {}); res.render(g_viewPath + 'did_manager', { title: 'Options', version: pk.util.claimer_version, first_use: options.first_use, default_card: options.default_card, last_default_card: options.default_card, startup_identifier: options.startup_identifier, startup_pin: options.startup_pin, share_consented_claims_html: share_consented_claims_html, auto_sync_html: auto_sync_html, self_asserted_personas_html: self_asserted_personas_html, chapi_wallet_html: chapi_wallet_html, clientArray: retrievedClients, current_manager_page: 'manager_options', pageHeading: 'Options' }); } function dispatchOptions(subPage){ var options = { devices: document.getElementById('sub_page_devices'), preferences: document.getElementById('sub_page_preferences') }; for (var key in options){ var option = options[key]; if (key === subPage){ option.classList.add('clms_1'); option.classList.remove('clms_0'); } else{ option.classList.remove('clms_1'); option.classList.add('clms_0'); } } } async function processOptionsResponse(pk, req, res){ var newOptions = simpleFormToJson('wallet_options'); var currentOptions = await getManagerOptions(); var reload_cards = false; if (currentOptions.self_asserted_personas !== newOptions.self_asserted_personas){ reload_cards = true; } if (currentOptions.chapi_wallet !== newOptions.chapi_wallet){ register_chapi_wallet(newOptions.chapi_wallet); } await setManagerOptions(newOptions); if (reload_cards){ dispatchManager('personas'); pk.ptools.paint_cards('selected'); } managerNotification("Options have been saved", 'alert-warning', true); } function register_chapi_wallet(mode){ var mediator = pk.util.config.content_modules.wallet.chapi_mediator + '?origin=' + encodeURIComponent(window.location.origin); var wallet_location = window.location.origin + '/wallet/chapi/'; pk.util.log_debug('Registering wallet...'); // Registers this demo wallet with the current user's browser, // from install-wallet.js registerWalletWithBrowser(mediator, wallet_location) .catch(e => console.error('Error in registerWalletWithBrowser:', e)); async function registerWalletWithBrowser() { try { await credentialHandlerPolyfill.loadOnce(mediator); } catch(e) { console.error('Error in loadOnce:', e); } pk.util.log_debug('Polyfill loaded.'); const workerUrl = wallet_location + 'wallet_worker.html'; pk.util.log_debug('Installing wallet worker handler at:', workerUrl); const registration = await WebCredentialHandler.installHandler({url: workerUrl}); await registration.credentialManager.hints.set( 'test', { name: 'TestUser', enabledTypes: ['VerifiablePresentation', 'VerifiableCredential', 'AlumniCredential'] }); pk.util.log_debug('Wallet registered.'); } } function optionsAddToHomeScreen(){ var orange = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAABr0lEQVR4Xu3VgQkAIAzEQDuNYzq6gmOE6wZJeDr37Ltc1sAInG37wQRu9xU43ldggesG4nx+sMBxA3E8CxY4biCOZ8ECxw3E8SxY4LiBOJ4FCxw3EMezYIHjBuJ4Fixw3EAcz4IFjhuI41mwwHEDcTwLFjhuII5nwQLHDcTxLFjguIE4ngULHDcQx7NggeMG4ngWLHDcQBzPggWOG4jjWbDAcQNxPAsWOG4gjmfBAscNxPEsWOC4gTieBQscNxDHs2CB4wbieBYscNxAHM+CBY4biONZsMBxA3E8CxY4biCOZ8ECxw3E8SxY4LiBOJ4FCxw3EMezYIHjBuJ4Fixw3EAcz4IFjhuI41mwwHEDcTwLFjhuII5nwQLHDcTxLFjguIE4ngULHDcQx7NggeMG4ngWLHDcQBzPggWOG4jjWbDAcQNxPAsWOG4gjmfBAscNxPEsWOC4gTieBQscNxDHs2CB4wbieBYscNxAHM+CBY4biONZsMBxA3E8CxY4biCOZ8ECxw3E8SxY4LiBOJ4FCxw3EMezYIHjBuJ4Fixw3EAcz4IFjhuI41mwwHEDcbwHzbU9aHcbxWYAAAAASUVORK5CYII="; var factory = new pk.ptools.personaCollectionFactory(); factory.load() .then(result => { pk.util.log_debug(result); }, err =>{ pk.util.log_error('optionsAddToHomeScreen','error in New Identity' + err); }); // deferredPwaPrompt.prompt(); } async function optionsExchange(){ var exchangeStatus = await pk.pexchange.exchange(); var removeNotification = managerNotification(exchangeStatus); function download(content, filename, contentType) { if(!contentType) contentType = 'application/octet-stream'; var a = document.createElement('a'); var blob = new Blob([content], {'type':contentType}); a.href = window.URL.createObjectURL(blob); a.download = filename; a.click(); } } function optionsAddDevice(role){ newDeviceManager = pk.hub_module.deviceManager(); newDeviceManager.addDevice(role, managerNotification) .then(result => { if (result){ var rendezvousDiv = document.createElement('div'); var devicesEl = document.getElementById('sub_page_devices'); var title, buttonText; if (role === 'invite'){ title = 'Add a new Cyber Identity Hub'; buttonText = 'Add new Hub' } else{ title = 'Become a clone of a partner Hub'; buttonText = 'Clone partner Hub' } devicesEl.innerHTML = '<div>\ <b> ' + title + ' </b>\ <hr/>\ <div id="current_step" class="pt-3">\ <label for="rendezvous">Enter your email address</label>\ <input type="email" class="form-control" id="rendezvous"><br/>\ <button class="btn btn-success" type="button" onclick="pk.pmanager.executeDeviceAddition(\'' + role + '\');">' + buttonText + '</button>\ </div></div>'; } }, err => { reject(err); }) } async function optionsRemoveWallet(){ await optionsRemoveWalletWorker(); } async function optionsRemoveWalletWorker(){ try{ var personas = pk.ptools.getPersonas(); for (var id in personas){ var cred = personas[id]; if (cred.kind !== "credential"){ continue; } var credentialInfo = await pk.token.requestCredentialInfo( cred.claim_source, { wallet_proof: 'deleted', }); var smartCredential = cred.vc_constants.type[cred.vc_constants.type.length - 1]; var iframeUrl = smartCredential + "/register?wallet_proof=" + credentialInfo.wallet_proof; try{ await siop_remote_cookie_op(iframeUrl, 'smart credential offline'); } catch(err){ managerNotification('Unable to delete wallet because ' + err + '. Please try later.', 'alert-warning', true); return; } } deleteDatabase("wallet_op"); deleteDatabase("wallet_content"); deleteDatabase("local_storage"); if ('caches' in window) { caches.keys() .then(function(keyList) { return Promise.all(keyList.map(function(key) { return caches.delete(key); })); }) } if(window.navigator && navigator.serviceWorker) { navigator.serviceWorker.getRegistrations() .then(function(registrations) { for(let registration of registrations) { registration.unregister(); } }); } var res = document.cookie; var multiple = res.split(";"); for(var i = 0; i < multiple.length; i++) { var key = multiple[i].split("="); document.cookie = key[0]+" =; expires = Thu, 01 Jan 1970 00:00:00 UTC"; } localStorage.clear(); for (var app in pk.util.operator_profile.wallet_app_group){ var selectedApp = pk.util.operator_profile.wallet_app_group[app]; var iframeUrl = selectedApp + '?clear_cookies=true'; await siop_remote_cookie_op(iframeUrl, selectedApp + ' is offline'); } window.location = 'https://www.google.com'; function deleteDatabase(dbName){ var DBDeleteRequest = window.indexedDB.deleteDatabase(dbName); DBDeleteRequest.onerror = function(event) { console.log("Error deleting database: " + dbName); }; DBDeleteRequest.onsuccess = function(event) { console.log("Database deleted: " + dbName); }; } } catch(err){ alert("RemoveWalletWorker: " + err); } } function siop_remote_cookie_op(iframeUrl, error){ return new Promise((resolve, reject) => { var launchIframeString = '\ <div style="display: none;">\ <iframe id="wallet_iframe"\ width="300"\ height="200"\ src="' + iframeUrl + '">\ </iframe>\ </div>'; var timer = setTimeout(function () { reject(error); }, 3000); var listener = window.addEventListener("message", (event) => { clearTimeout(timer); resolve(event.data); }, false); var remoteCookieEl = document.getElementById('remote_cookie_reader'); remoteCookieEl.innerHTML = launchIframeString; }); } function executeDeviceAddition(){ var sharedSecret16; return new Promise((resolve, reject) => { var rendezvousId; var rendezvousEl = document.getElementById('rendezvous'); var rendezvousId = rendezvousEl.value; newDeviceManager.executeDeviceAddition(rendezvousId) .then(result => { if (result){ sharedSecret16 = result; return newDeviceManager.exportFlockMembershipCipher(); } }, err => { reject(err); }) .then(result => { if (result){ var flockMembershipCipher = result; var rand = sharedSecret16[4] % shakespeare.QUOTES.length; var phrase = shakespeare.QUOTES[rand]; var currentStepEl = document.getElementById('current_step'); currentStepEl.innerHTML = '<p>Carefully check these phrases on both devices:<p>\ <br/><h3>' + phrase + '</h3><br/><br/>\ <button class="btn btn-success" type="button" onclick="pk.pmanager.conveyFlockMembershipCipher(\'' + rendezvousId + '\', \'' + flockMembershipCipher + '\');">Yes - the phrases match</button>'; resolve(true); } }) }); } function conveyFlockMembershipCipher(rendezvousId, flockMembershipCipher){ return new Promise((resolve, reject) => { newDeviceManager.conveyFlockMembershipCipher(rendezvousId, flockMembershipCipher) .then(result => { if (result){ window.location.reload(); resolve(true); } }, err => { reject(err); }) }); } function simpleFormToJson(formId){ var formEl = document.forms[formId]; if (!formEl){ pk.util.log_error('simpleFormToJson', 'Error getting formId', formId); return null; } var inputs = formEl.getElementsByTagName("input"); var claims = {}; for (var icount=0; icount < inputs.length; icount++){ var el = inputs[icount]; if (el.type === 'checkbox'){ claims[el.id] = el.checked; } else{ claims[el.id] = el.value.trim(); } } var inputs = formEl.getElementsByTagName("select"); for (var icount=0; icount < inputs.length; icount++){ var el = inputs[icount]; claims[el.id] = el.value.trim(); } return claims; } // proper case function based on stack overflow function dashlessProperCase(s) { return s.replace(/\_/g, ' ').toLowerCase().replace(/^(.)|\s(.)/g, function($1) { return $1.toUpperCase(); }); } /* ** TBD - Remove when we verify no longer used function addIssuerModalClick(){ var issuerEl = document.getElementById('credential_issuer_url'); var selectedPersona = pk.ptools.getPersona('selected'); var naturalPersona = pk.ptools.getPersona('natural'); pk.app.renderDispatcher('get', '/wallet/invoke_credential_issuer', { issuer: issuerEl.value, selectedPersona: selectedPersona, natural_id: naturalPersona.id }); } async function completeAddCardCredential(pk, req, res){ var issuer_metadata = req.body.issuer_metadata; var candidatePersona = req.body.candidatePersona; var endpoint = issuer_metadata.authorization_endpoint; var serviceUrl = window.location.origin; var client_id = candidatePersona.id; var redirect_uri = serviceUrl + NEW_ISSUER_REGISTRATION_RESPONSE_ENDPOINT; var parameters = { client_id: client_id, redirect_uri: redirect_uri, response_type: 'id_token token', login_hint: candidatePersona.id, // nonce: candidatePersona.id, nonce: await pk.simple_crypto.randomString(), scope: 'openid holder_token Covid-ImmunityTest-PV7' }; var parameterString = pk.util.createParameterString(parameters); res.redirect(endpoint + parameterString); res.end(); } */ function managerNotification(message, className, dismissible) { var msg = { action: 'managerNotification', message: message, className: className, dismissible: dismissible }; pk.util.masterNotification(msg); } ///////////////////////////////////////////////////////////////////// const siop_as_client = "Credential Issuer Registration"; ////////////////////////////////////////////////////////////////////////// async function create_persona_with_credential(credentialIssuer, next_step, claims){ var persona_id = await pk.ptools.locate_or_add_credential(credentialIssuer); if (!persona_id){ throw ('no persona_id located or added in create_persona_with_credential'); } var result = await request_credential(persona_id, credentialIssuer, next_step, claims); return result; } ////////////////////////////////////////////////////////////////////////////////// async function process_code(req, res){ try{ var walletDbInfo = pk.dbs['wallet']; var stsDbInfo = pk.dbs['sts']; var code = req.query.code; if (!code){ throw 'code not specified in process_code'; } var state = req.query.state; if (!state){ throw 'state not specified in process_code'; } var stateParams = pk.util.parseParameterString(state); var access_token_endpoint = stateParams['tok_ept']; var cred_endpoint = stateParams['cred_ept']; var cred_persona = await walletDbInfo.provider.getDocument(walletDbInfo, 'accounts', stateParams.sub); if (!cred_persona){ throw('process_code: cred_persona not found.'); } var cred_key = await stsDbInfo.provider.getDocument(stsDbInfo, 'keys', cred_persona.kid); if (!cred_key){ throw('process_code: cred_key not found.'); } var redirect = pk.sts.selfIssuedIssuerIdentifier(); // what is posted to get access token from code endpoint var postData = { "grant_type": "authorization_code", "code": code, "redirect_uri": redirect, "client_id": cred_persona.client_id, "code_verifier": cred_persona.data.pkce }; var options = { url: access_token_endpoint, method: "POST", parseJsonResponse: true, headers: [ { name: 'Accept', value: 'application/json' }, { name: 'Content-type', value: 'application/json' } ], postData: postData } pk.util.log_protocol('Code flow access_token Request', 'access_token_issuer', options); var tokenResponse = await pk.util.jsonHttpData(options); pk.util.log_protocol('TokenResponse received from code access_token Request', 'issuer', tokenResponse); /* TODO: remove bugfix if (!tokenResponse.credential_format){ tokenResponse.credential_format = 'w3cvc-jsonld'; } tokenResponse.metadata.persona_info.card_formats = { "card_subject_name": { "position":"absolute","bottom":"180px","color":"#E2F0D9","width":"350px","text-align":"center","font-size":"1.1em","font-family":"serif","font-weight":"bolder" }, // "card_logo_url": { "position":"absolute","bottom":"120px","color":"#FFFFFF","width":"350px","text-align":"center","font-size":"1.1em","font-family":"serif","font-weight":"bolder" }, "card_title": { "position":"absolute","bottom":"25px","color":"#FFFFFF","width":"350px","text-align":"center","font-size":"1.3em","font":"Calibri","font-family":"sans-serif" } }; tokenResponse.metadata.persona_info.output_credential_colors = '#118848'; */ if (!tokenResponse.credential_format){ tokenResponse.credential_format = 'w3cvc-jsonld'; } tokenResponse.metadata.persona_info.card_formats = { "card_subject_name": { "position":"absolute","bottom":"152px","color":"#E2F0D9","margin-left":"30px","width":"320px","font-size":"1.1em","font-family":"sans-serif","font-stretch":"expanded","font-style":"oblique" }, // "card_logo_url": { "position":"absolute","bottom":"120px","color":"#FFFFFF","width":"350px","text-align":"center","font-size":"1.1em","font-family":"serif","font-weight":"bolder" }, "card_title": { "position":"absolute","bottom":"25px","color":"#FFFFFF","width":"350px","text-align":"center","font-size":"1.3em","font":"Calibri","font-family":"sans-serif","font-stretch":"expanded" } }; cred_persona.claim_source = { credential_endpoint: cred_endpoint, access_token: tokenResponse.access_token, token_type: tokenResponse.token_type, refresh_token: tokenResponse.refresh_token, credential_format: tokenResponse.credential_format, credential_type: tokenResponse.credentialType }; if (tokenResponse.metadata){ cred_persona.issuer_url = tokenResponse.metadata.issuer_url; cred_persona.issuer_name = tokenResponse.metadata.issuer_name; cred_persona.card_formats = tokenResponse.metadata.persona_info.card_formats; cred_persona.card_title = tokenResponse.metadata.persona_info.card_title; cred_persona.card_color = tokenResponse.metadata.persona_info.output_credential_colors; cred_persona.resources = tokenResponse.metadata.persona_info.resources; cred_persona.credential_description = tokenResponse.metadata.description; cred_persona.vc_constants = { type: tokenResponse.metadata.type }; var options = { url: tokenResponse.metadata.persona_info.resources_endpoint + "?resources=" + cred_persona.resources, method: 'GET', parseJsonResponse: true, headers: [ { name: 'Accept', value: 'application/json' } ] }; var resources; try { resources = await pk.util.jsonHttpData(options) } catch(err){ pk.util.log_error('ERROR OBTAINING Token METADATA', err); throw(err); } if (resources){ if (resources.card_design){ cred_persona.card_design = resources.card_design; } if (resources.wallet_render){ cred_persona.wallet_render = resources.wallet_render; } if (resources.scope_claim_map){ cred_persona.scope_claim_map = resources.scope_claim_map; } if (resources.display_render){ cred_persona.display_render = resources.display_render; } } } else{ cred_persona.credential_description = 'complete - no metadata'; } // cred_persona.capabilities = tokenResponse.capabilities cred_persona.kind = 'credential'; await walletDbInfo.provider.createOrUpdateDocument(walletDbInfo, 'accounts', cred_persona); // add the card_subject_name if available if (tokenResponse.metadata.persona_info.card_subject_name_components){ var retrieved_cred = await pk.token.retrieveVerifiableCredential(cred_persona, { credential_id: cred_persona.id, credential_sub: cred_persona.id }); cred_persona.card_subject_name = createCardSubjectName(retrieved_cred, tokenResponse.metadata.persona_info.card_subject_name_components); await walletDbInfo.provider.createOrUpdateDocument(walletDbInfo, 'accounts', cred_persona); } window.location = pk.sts.selfIssuedIssuerIdentifier() + "?page=personas"; /* var postData = { "request": jws_compact }; // var postData = 'request=' + jws_compact; var cred_req_options = { url: cred_endpoint, method: 'POST', headers: [ { name: 'Accept', value: 'application/json' }, { name: 'Authorization', value: 'Bearer ' + credential_info.access_token }, { name: 'Content-Type', value: 'application/json' } // { name: 'Content-Type', value: 'x-www-form-urlencoded' } ], postData: postData } var credResponseString = await pk.util.jsonHttpData(cred_req_options); */ /* var body = JSON.stringify(postData); var headers = new Headers(); headers.append('Accept', 'application/json'); headers.append('Content-Type', 'application/json'); headers.append('Authorization', 'Bearer ' + credential_info.access_token); headers.append('Content-Length', body.length.toString()); a console.log("Accept: " + headers.get('Accept')); console.log("Content-Type: " + headers.get('Content-Type')); console.log("Authorization: " + headers.get('Authorization')); console.log("Content-Length: " + headers.get('Content-Length')); var credResponse = await _do_fetch( cred_endpoint, "POST", headers, body );f */ //var result = await process_credential_issuer_response(req, res, credential_info, stateParams.next_step); } catch(err){ console.log('ERROR IN PROCESS_CODE', err); throw err; } } function createCardSubjectName(retrieved_cred, card_subject_name_components){ if (!retrieved_cred || !card_subject_name_components){ return; } var spacer = ''; var card_subject_name = ''; for (var i=0; i<card_subject_name_components.length; i++){ var claimName = card_subject_name_components[i]; var component; if (!retrieved_cred.credential || !retrieved_cred.credential.credentialSubject){ break; } if (claimName.startsWith('vc.')){ component = retrieved_cred.credential.credentialSubject[claimName.substr(3)]; } else{ component = retrieved_cred.credential[claimName]; } if (component){ card_subject_name += spacer + component; spacer = ' '; } } return card_subject_name; } function _do_fetch(url, method, headers, body){ return new Promise((resolve, reject) => { var init = { method: method, mode: "cors", credentials: "include", headers: headers, body: body } window.fetch( new URL(url), init ) .then(response => { if (!response){ reject('no response'); } return response.json(); }) .then(data => { if (!data){ reject('no data'); } console.log('Success:', data); }) .catch((error) => { console.error('Error:', error); reject(error); }); }); } async function process_credential_issuer_response(req, res, params, next_step){ var dbInfo = pk.dbs['wallet']; var id_token; if (location.hash && location.hash.startsWith('#')){ if (debugging_credential_issuer){ alert("Restart with '?'"); return; } params = {}; // option to proceed without # should be removed in prod var postBody = location.hash.substring(1); var regex = /([^&=]+)=([^&]*)/g; var m; while (m = regex.exec(postBody)) { params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); } } else if (!params){ // TODO: remove this // only used for testing while under development params = req.query; } var error_description; try { if (params.error){ var msg = params.error; if (params.error === 'invalid_request'){ if (params.error_description){ msg = params.error_description; if (params.error_description.includes('nonce')) { msg = 'Your credential has been added.'; } } } var parameters = { page: "personas", msg: msg } res.redirect(pk.util.WALLET_ENDPOINT + pk.util.createParameterString(parameters)); res.end(); return; } //debugger; error_description = 'token is empty or corrupt'; var id_token = await pk.token.processOPTokenResult(params); error_description = 'token sub is not valid'; var existingCredDoc = await dbInfo.provider.getDocument(dbInfo, 'credential_issuer_claims', id_token.sub); if (id_token.sub !== existingCredDoc.id){ throw("Error: token sub is not valid"); } if (id_token.iss !== existingCredDoc.issuer){ throw("Error: credential issuer is not valid"); } var credential_issuer_info = existingCredDoc; credential_issuer_info.issuer_name = id_token.issuer_name; credential_issuer_info.sub = id_token.sub; if (credential_issuer_info.vc_constants === undefined){ credential_issuer_info.vc_constants = id_token.vc_constants; } else{ for (var key in id_token.vc_constants){ credential_issuer_info.vc_constants[key] = id_token.vc_constants[key]; } } credential_issuer_info.claim_source = id_token.claim_source; if (credential_issuer_info.persona_info === undefined){ credential_issuer_info.persona_info = id_token.persona_info; } else{ for (var key in id_token.persona_info){ credential_issuer_info.persona_info[key] = id_token.persona_info[key]; } } var personaClaims = { vc_constants: credential_issuer_info.vc_constants, card_title: credential_issuer_info.persona_info.card_title, credential_description: credential_issuer_info.persona_info.credential_description, claim_source: credential_issuer_info.claim_source, scope: credential_issuer_info.persona_info.scope, resources: credential_issuer_info.persona_info.resources, card_text: credential_issuer_info.persona_info.card_text, iwi: credential_issuer_info.persona_info.iwi, kid: existingCredDoc.kid, kind: "credential" } var credentialInfo = await pk.token.requestCredentialInfo( credential_issuer_info.claim_source, { wallet_proof: pk.sts.selfIssuedIssuerIdentifier(), ancillary_claims: true, resources: personaClaims.resources }); // get the ancillary claims for (var key in credentialInfo.ancillary_claims){ personaClaims[key] = credentialInfo.ancillary_claims[key]; } for (var key in credentialInfo.resources){ personaClaims[key] = credentialInfo.resources[key]; } // if no card_design img then use credential_colors if (!personaClaims.card_design){ if (credential_issuer_info.persona_info.credential_colors){ personaClaims.card_design = credential_issuer_info.persona_info.credential_colors; } else{ personaClaims.card_design = '#0000FF #FFFFFF #CAE5FF'; // default to ugly color to force change } } if (personaClaims['scope_claim_map']){ personaClaims['scope_claim_map'] = JSON.parse(personaClaims['scope_claim_map']); } if (personaClaims['recommended_apps']){ personaClaims['recommended_apps'] = JSON.parse(personaClaims['recommended_apps']); var apps = personaClaims['recommended_apps'].recommended_apps; for (var i in apps){ var websitesInfo = { id: apps[i].name, url: apps[i].url } await dbInfo.provider.createOrUpdateDocument(dbInfo, 'websites', websitesInfo, null); } } if (!personaClaims.kid){ personaClaims.kid = pk.ptools.getPersona('id', existingCredDoc.id).kid; } await pk.ptools.updatePersona(existingCredDoc.id, personaClaims); error_description = 'Unable to save updated credential issuer doc'; var savedUpdate = await dbInfo.provider.createOrUpdateDocument(dbInfo, 'credential_issuer_claims', credential_issuer_info, null); pk.util.log_debug('Updated credential issuer doc was saved'); pk.util.log_debug('Wallet Manager SuccessPage: ' + next_step); if (!next_step){ // set up url for returning to wallet var parameters = { page: "personas", selected: id_token.aud } next_step = pk.sts.selfIssuedIssuerIdentifier() + pk.util.createParameterString(parameters, false); } registerSmartCredential(res, credentialInfo.wallet_proof, credential_issuer_info.vc_constants, next_step); // res.redirect(pk.util.WALLET_ENDPOINT + pk.util.createParameterString(parameters)); // res.end(); } catch (err){ res.render(g_viewPath + 'error_received', { component: siop_as_client, error: 'invalid_token', error_description: error_description + err }); } } function registerSmartCredential(res, wallet_proof, vc_constants, redirect){ if (vc_constants){ var type = vc_constants.type; if (type){ var credential_type = type[type.length -1]; if (credential_type.startsWith('https://')){ var url = credential_type + '/register'; var parameters = { wallet_proof: wallet_proof, redirect: redirect }; url += pk.util.createParameterString(parameters, false); res.redirect(url); res.end(); return; } } } res.redirect(redirect); res.end(); } function getObjectUrl(url){ return new Promise((resolve, reject) => { fetch(url) .then(res => res.blob()) .then(res => { const objectURL = URL.createObjectURL(res); resolve(objectURL); return; }); reject("Error getting " + url); }); } /* Supports installation of a fresh wallet that immediately obtains a credential when the subject issyes a GET to <siop_url>?req_cred=<credential_issuer_url> Detail: Redirects the newly created wallet to: <credential_issuer_url>?iss=<siop_url>&login_hint=generate@credential_issuer_redirect_url" */ async function process_req_cred(req, res){ pk.util.log_debug('--- PROCESS REQ_CRED ---'); pk.util.log_debug('query parameters', req.query); var cred_issuer = req.query.req_cred; var claims = req.query.claims; var next_step = req.query.next_step; var potential_error_message = 'Unable to obtain credential_issuer metadata'; try { await create_persona_with_credential(cred_issuer, next_step, claims); } catch(err){ var alert_message = potential_error_message + ": " + err; alert("Error in process_req_cred: " + alert_message); } } function pairwiseKeyConfig(){ var wallet_config = pk.util.operator_profile.wallet_config_group; var expandedKeyType = pk.key_management.expandValidKeyType(wallet_config.persona_keyType); if (!expandedKeyType){ pk.util.log_error('operator configurtion error - personaKeyType is invalid combination: ', wallet_config.persona_keyType); } return expandedKeyType; } function didMethodConfig(){ var wallet_config = pk.util.operator_profile.wallet_config_group; return wallet_config.did_method; } function wallet_encrypt(alg, key, payload){ if (!alg){ console.trace('wallet_encrypt'); throw ('wallet_encrypt: fatal no alg'); } if (!key){ console.trace('wallet_encrypt'); throw ('wallet_encrypt: fatal no key'); } var iv_raw = forge.random.getBytesSync(16); // secret var secret = { iv: pk.base64url.encode(iv_raw), cipher: undefined } // cipher var cipher = forge.cipher.createCipher(alg, key.bytes()); cipher.start({iv: iv_raw}); cipher.update(payload); cipher.finish(); secret.cipher = pk.base64url.encode(cipher.output.bytes()); return secret; } function wallet_decrypt(alg, key, encrypted_payload){ if (!alg){ console.trace('wallet_decrypt'); throw ('wallet_decrypt: fatal no alg'); } if (!key){ console.trace('wallet_decrypt'); throw ('wallet_decrypt: fatal no key'); } var decipher = forge.cipher.createDecipher(alg, key.bytes()); decipher.start({iv: pk.base64url.decode(encrypted_payload.iv) }); decipher.update(forge.util.createBuffer(pk.base64url.decode(encrypted_payload.cipher))); var result = decipher.finish(); // check 'result' for true/false // outputs decrypted hex return decipher.output; }