oidc-lib
Version:
A library for creating OIDC Service Providers
1,436 lines (1,228 loc) • 45.8 kB
JavaScript
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¬ification=' + 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;
}