oidc-lib
Version: 
A library for creating OIDC Service Providers
684 lines (563 loc) • 21.2 kB
JavaScript
const VIEWPATH = '\\wallet\\views\\';
module.exports = {
	registerEndpoints: registerEndpoints,
	invokeAuthUserAgent: invokeAuthUserAgent,
	invokeConsentUserAgent: invokeConsentUserAgent,
	generateUserinfo: generateUserinfo,
	processVerifiedIdToken: processVerifiedIdToken,
	token_presentation_options: token_presentation_options,
	setConsentMode: setConsentMode,
	VIEWPATH: VIEWPATH
}
const moduleName = 'wallet';
var viewPath = null;
var pk = null;
// var userAccounts = null;
var dbInfo = null;
var token_presentation_values = {
	imposeFormPostResponseMode: false,
	vcFormat: 'verifiablePresentation'
};
function registerEndpoints(pkInput) {
	pk = pkInput;
	viewPath = VIEWPATH;
	// userAccounts = require('./data/accounts');
	// var claimsMgr = require("./claimsmgr");
	// claimsMgr.registerEndpoints(pk, userAccounts, getAccountBySub);
	pk.app.post('/wallet/auth_useragent_response', function(req, res){
		processAuthUserAgentResponse(pk, req, res);
	});
	pk.app.post('/wallet/consent_useragent_response', function(req, res){
		processConsentUserAgentResponse(pk, req, res);
	});
	pk.app.post('/wallet/process_ajax_request', function(req, res){
		processAjaxRequest(pk, req, res);
	});
	pk.app.get('/wallet/manager', function(req, res){
		pk.pmanager.manager(pk, req, res);
	});
	pk.app.get('/wallet/oauth_error', function(req, res){
		pk.pmanager.oauth_error(pk, req, res);
	});
	pk.app.get('/wallet/pickup_uri', function(req, res){
		process_pickup_uri(pk, req, res);
	});
	pk.app.get('/wallet/entry_point', function(req, res){
		return process_wallet_entry_point(pk, req, res);
	});
	pk.app.get('/wallet/oauth_error', function(req, res){
		pk.pmanager.oauth_error(pk, req, res);
	});
	// IMPORTANT
	// Additional 'wallet' endpoints are defined in
	// walletManager.js
}
function invokeAuthUserAgent(req, res, params, encoded_sts_state_bundle) {
	pk.util.log_debug('--- WALLET: INVOKE AUTH USERAGENT ---');	
	var content_module_state = {};
	pk.util.log_debug("WARNING:  login_hint has been disabled...");
	/*
	var validLoginHint = false;
	if (params.login_hint){
		var hint_persona = pk.ptools.getPersona('id', params.login_hint);
		if (hint_persona){
			content_module_state.sub = hint_persona.id;
			validLoginHint = true;
		}
	}
	if (!validLoginHint){
		content_module_state.sub = natural.id;		
	}
	*/
	pk.sts.applyAuthResponse(res, encoded_sts_state_bundle, content_module_state);
	return;
}
/*
placeholder if authUserAgentValidation required
function processAuthUserAgentResponse (pk, req, res){
	pk.sts.applyAuthResponse(res, req.body.encoded_sts_state_bundle, content_module_state);
}
*/
function processVerifiedIdToken(res, verified_id_token, encoded_sts_state_bundle){
	pk.util.log_debug('--- WALLET: PROCESS VERIFIED ID TOKEN ---');	
	var account = getAccountBySub(verified_id_token.sub);
	if (account === false){
		return;
	}
	content_module_state = {};
	content_module_state.sub = verified_id_token.sub;
	pk.sts.sendAuthResponse(res, encoded_sts_state_bundle, content_module_state);
}
async function updateConsentedToken(res, scopeInfo, encoded_sts_state_bundle, content_module_state){
	var consentedPersona;
	var config = pk.ptools.getPersona('natural').data.options;
	if (!content_module_state.explicitConsent 
		&& config.share_consented_claims 
		&& content_module_state.consentInfo
		&& content_module_state.consentInfo.idTokenContent
		&& pk.sts.isScopeConsented(content_module_state)){
			var id = content_module_state.consentInfo.credentialIssuerId;
			consentedPersona = pk.ptools.getPersona('id', id);
	}
	if (!consentedPersona){
		return false;
	}
	var claims = content_module_state.consentInfo.idTokenContent;
	content_module_state.consentInfo = scopeInfo;
	content_module_state.consentInfo.currentPersona = content_module_state.sub;
    var requestOptions = {
        sub: content_module_state.sub
    }
 
    var distributedClaims = await pk.token.retrieveVerifiableCredential(consentedPersona, requestOptions);
	
	if (consentedPersona.vc_constants && distributedClaims){
		var vcFormat = await pk.feature_modules['wallet'].code.token_presentation_options({ option: 'vcFormat' });
		if (vcFormat === 'verifiablePresentation'){
		  var vp = {
		    '@options': consentedPersona.vc_constants['@options'],
		    type: [ 'VerifiablePresentation'],
		    verifiableCredential: [ distributedClaims.JWT ]
		  }
		  claims['vp'] = vp;
		}
		else {
		  claims._claim_names = {};
		  claims._claim_sources = {};
		  var effective_class = consentedPersona.vc_constants.type[consentedPersona.vc_constants.type.length - 1];
		  claims._claim_names[effective_class] = 'vc1';
		  claims._claim_sources['vc1'] = distributedClaims;
		}
	}
	content_module_state.scope_claim_map = consentedPersona.scope_claim_map;
	for (var i=0; i<consentedPersona.scopes_and_creds.length; i++){
		var scred = consentedPersona.scopes_and_creds[i];
		if (content_module_state.consentInfo.scopeArray.includes(scred)){
			continue;
		}
		content_module_state.consentInfo.scopeArray.push(scred);
	}
	content_module_state.consentInfo.credentialIssuerId = consentedPersona.id;
	claims.sub = content_module_state.consentInfo.currentPersona;
	content_module_state.newIdTokenContent = claims;
	pk.sts.applyConsentResponse(res, encoded_sts_state_bundle, content_module_state);
	return (true);
}
async function invokeConsentUserAgent(res, scopeInfo, client_info, encoded_sts_state_bundle, content_module_state) {
	pk.util.log_debug('--- WALLET: INVOKE CONSENT USERAGENT ---');
	// skip the UI experience if previous approval has been obtained
	// the user has configured the wallet for this and the RP is good with it 
	if (await updateConsentedToken(res, scopeInfo, encoded_sts_state_bundle, content_module_state)){
		return;
	}
	var client_info_class = 'clms_0';
	var client_name_warning_class = 'clms_0';
	// client_info will be set to null rather than undefined if startup was for wallet/applicaiton
	if (client_info !== null){
		client_info_class = 'clms_1';
		var client_uri = client_info.client_uri;
		if (client_uri === undefined){
			client_uri = client_info.redirect_uri;
		}
		var client_name = client_info.client_name;
		if (client_name === undefined){
			client_name = client_uri
		}
		var company_logo = client_info.company_logo;
		if (company_logo === undefined){
			company_logo = "https://undefined.logo.uri"; // TODO:  define default
		}
		var client_name_warning = false;
		var redirect_host = pk.util.url(client_info.redirect_uri);
		
		var client_host;
		if (client_info.client_uri !== undefined){
			client_host = pk.util.url(client_info.client_uri);
		}
		
		if (client_host === undefined || client_host.hostname !== redirect_host.hostname){
			client_uri = client_info.redirect_uri;
			client_name_warning = true;
		}
	}
	var scopeInfoString = JSON.stringify(scopeInfo);
	var scopeInfo64 = pk.base64url.encode(scopeInfoString);
	var previousConsentInfo = content_module_state.consentInfo;
	if (previousConsentInfo === undefined){
		previousConsentInfo = {};
	}
	var consentInfoString = JSON.stringify(previousConsentInfo);
	var consentInfo64 = pk.base64url.encode(consentInfoString);
	var natural = pk.ptools.getPersona('kind', 'natural'); 
	var options = natural.data.options;
	var identifier_class = options.startup_identifier ? 'clms_1' : 'clms_0';
	var pin_class = options.startup_pin ? 'clms_1' : 'clms_0';
	if (client_name_warning){
		client_name_warning_class = 'clms_1';
	}
	// var cardset_blurb = 'Create and use persona cards to control what kind of data you share with different websites  ';
	var cardset_blurb = 'Select an ID';
	var contentModuleState64 = pk.base64url.encode(JSON.stringify(content_module_state));
	res.render(viewPath + 'get_consent', {
		title: 'Getting Consent',
		client_name: client_name,
		client_uri: client_uri,
		client_redirect_uri: client_info.redirect_uri,
		cardset_blurb: cardset_blurb,
		company_logo: company_logo,
		danger:  client_name_warning_class,
		client_info_class: client_info_class,
		identifier_class: identifier_class,
		pin_class: pin_class,
		scope_info: scopeInfo64,
		consent_info: consentInfo64,
		content_module_state: contentModuleState64,
		encoded_sts_state_bundle: encoded_sts_state_bundle
	});
}
function setConsentMode(mode)
{
	if (mode){
		var cmsString = document.getElementById("content_module_state").value;
		var content_module_state = JSON.parse(pk.base64url.decode(cmsString));
		if (content_module_state.login_hint){
			var login_persona = pk.ptools.getPersona("id", content_module_state.login_hint);
			if (login_persona){
				if (login_persona.kind === 'credential'){
					pk.ptools.output_card(true);
					return;
				}				
			} 
		}
	}
	pk.util.setElementVisibility('new_consent', !mode);
	pk.util.setElementVisibility('existing_consent', mode);
}
async function processConsentUserAgentResponse (pk, req, res){
	pk.util.log_debug('--- WALLET: PROCESS CONSENT USER AGENT RESPONSE ---');	
	var params = req.body;
	pk.util.log_detail('params', params);
	// TODO:  check with server and ensure encryption
	var natural = pk.ptools.getPersona('kind', 'natural'); 
	var options = natural.data.options;
	if (options.startup_pin){
		if (options.startup_pin !== params.holder_credential){
    		pk.pmanager.managerNotification('Invalid Wallet PIN', 'alert-danger', true);
    		return;
		}
	}
	var content_module_state = JSON.parse(pk.base64url.decode(params.content_module_state));
	if (params.error){
		content_module_state.error = params.error;
	}
	else{
		content_module_state.scope_claim_map = params.scope_claim_map;
		
		var consentInfoString = pk.base64url.decode(params.scope_info);
		var consentInfo = JSON.parse(consentInfoString);
		// add creds to consent scopeArray since creds behave like scopes
		for (var i=0; i<params.scopes_and_creds.length; i++){
			var scred = params.scopes_and_creds[i];
			if (consentInfo.scopeArray.includes(scred)){
				continue;
			}
			consentInfo.scopeArray.push(scred);
		}
		if (params.accepted === false){
			var index = consentInfo.scopeArray.indexOf('openid');
			if (index > -1) {
			  consentInfo.scopeArray.splice(index, 1);
			}		
		}
		if (params.currentPersona !== undefined){
			consentInfo.currentPersona = params.currentPersona;
		}
		if (params.credentialIssuerId !== undefined){
			consentInfo.credentialIssuerId = params.credentialIssuerId;
		}
		// TODO: make it possible to use the same thum that will be used in the final token
		// with persona keys, that could be the thumbprint of the persona key.
		// with pairwise keys, it is the thumbprint of the pairwise key
		params.claims.sub = consentInfo.currentPersona;
		content_module_state.newIdTokenContent = params.claims;
		content_module_state.consentInfo = consentInfo;
	}
	pk.sts.applyConsentResponse(res, params.encoded_sts_state_bundle, content_module_state);
}
function processAjaxRequest (pk, req, res){
	pk.util.log_debug('--- WALLET: PROCESS AJAX REQUEST ---');
	var params = req.body;
	pk.util.log_detail('params', params);
	if (validateStartupCredentials(res, params.startup_identifier, params.startup_pin) !== true){
		return;		
	}
	switch (params.op){
		case 'check_credentials':
			sendFauxAjaxSubmit(res);
			break;
		default:
			sendFauxAjaxError(res, 'unknown_op', params.op);
	}
}
function generateUserinfo(res, params, consentInfo, accessTokenContent){
	pk.util.log_debug('--- WALLET: GENERATE USERINFO ---');	
	pk.util.log_debug('Generating userInfo in wallet\r\n');
	pk.util.log_detail('sub', accessTokenContent.sub);
	var account = getAccountBySub(accessTokenContent.sub);
	pk.sts.submitUserinfoResponse(res, moduleName, params, consentInfo, content_module_state.consentInfo.tokenContent);
}
/*********************************************************************************************/
// Simplistic stubs
function validateStartupCredentials(res, startup_identifier, startup_pin){
	var options = pk.ptools.getPersona('kind', 'natural').options;
	if (options.startup_identifier){
		if (!startup_identifier){
			return sendFauxAjaxError(res, 'invalid_authorization_parameters');
		}
		startup_identifier = startup_identifier.toLowerCase();
		if (options.startup_identifier.toLowerCase() !== startup_identifier){
			return sendFauxAjaxError(res, 'incorrect startup id');
		}
	}
	if (options.startup_pin){
		if (!startup_pin){
			return sendFauxAjaxError(res, 'invalid_authorization_parameters');
		}
		if (options.startup_pin !== startup_pin){
			pk.util.log_detail('startup id entered', startup_identifier);
			pk.util.log_detail('startup pin entered', startup_pin);
			return sendFauxAjaxError(res, 'invalid_pin');
		}
	}
	return true;
}
function getAccountBySub(sub){
	for(var key in userAccounts){
		if (userAccounts[key].sub === sub){
			return userAccounts[key];
		}
	}
}
/*********************************************************************************************/
// utility
function sendFauxAjaxError(res, error, error_description){
	pk.util.log_detail('SENDING AJAX ERROR', error);
	var payload = {
		kind: 'error',
		detail: {
			error: error,
			error_description: error_description
		}
	};
	res.json(payload);
	return false;
}
function sendFauxAjaxSubmit(res){
	var payload = {
		kind: 'submit',
	};
	res.json(payload);
	return true;
}
async function token_presentation_options(params){
	switch (params.option){
		case 'share_consented_claims':
			var config = pk.ptools.getPersona('natural').data.options;
			return config['share_consented_claims'];
		case 'tokenSigningKey':
			return await getPersonaSigningKey(params);
			break;
		case 'imposeFormPostResponseMode':
		case 'vcFormat':
			if (params.value){
				token_presentation_values[params.option] = params.value;
			}
			else{
				return token_presentation_values[params.option]; 
			}
			break;
	}
}
async function getPersonaSigningKey(params){
	try{
		var sub = params.sub;
		if (sub === undefined){
			throw('tokenSigningKey requested but no sub specified');
		}
		// use the did index to look up the key with the correct did
		var keyAndStore = await pk.key_management.loadSingleKey({ dictionary: {"did": sub } });
	    return keyAndStore.keyObject;
	}
	catch(err){
		throw(err);
	}
}
function process_pickup_uri(pk, req, res){
	var pickup_uri = req.query.pickup_uri;
	if (!pickup_uri){
		res.statusCode = 400;
		res.end();
		return;
	}
	var credential_pickup_def = pk.util.operator_profile.wallet_config_group.credential_pickup;
	var matching_issuer = '';
	if (credential_pickup_def){
		for (var key in credential_pickup_def){
			if (pickup_uri.startsWith(key)){
				matching_issuer = credential_pickup_def[key];
				break;
			}
		}
	}
	if (!matching_issuer){
		res.statusCode = 400;
		res.end();
		return;
	}
	// handle bug in OH rquest
	var pickup_uri_offset = req.originalUrl.indexOf('pickup_uri=');
	pickup_uri = req.originalUrl.substr(pickup_uri_offset + 11);
	pickup_uri = encodeURIComponent(pickup_uri);
	var wallet_url = pk.sts.selfIssuedIssuerIdentifier();
	var claimsObj = {
		"id_token":{
			"shc":{
				value: pickup_uri
			} 
		}
	}
	var claims = JSON.stringify(claimsObj);
	var url = wallet_url + '?req_cred=' + matching_issuer + '&claims=' + claims + '&next_step=' + wallet_url + '%3Fpage%3Dpersonas';
	window.location = url;
}
async function process_wallet_entry_point(pk, req, res){
	if (req.query.iss && req.query.login_hint){
		return process_oidc_initiate_login(pk, req, res);		
	}
}
async function process_oidc_initiate_login(pk, req, res){
	try{
		var error_message = 'Error getting oidc_initiate_login params';
		var iss = req.query.iss;
		var login_hint = req.query.login_hint;
		var requiredProperties = {
			iss: iss
		}
		// check existience of credential
		var existing_credential;
		var registerResult;
		// var cred_persona = pk.ptools.getPersona('target', requiredProperties);
		if (!existing_credential){
			var error_message = 'Error getting credential issuer metadata';
		    var metadataString = await pk.token.requestOPMetadata(iss);
		    error_message = 'Unable to parse credential_issuer metadata';
		    var credential_issuer_metadata = JSON.parse(metadataString);
		    var registration_endpoint = credential_issuer_metadata.registration_endpoint;
		    if (!registration_endpoint){
			    error_message = 'Unable to retrieve credential_issuer registration endpoint';
			    throw("No registration endpoint");
		    }
		    var postData = {
		        redirect_uris: [ pk.sts.selfIssuedIssuerIdentifier() ]
    		}
	    	var http_options = {
				url: registration_endpoint,
				method: 'POST',
				parseJsonResponse: true,
	    		headers: [ 
		    		{ name: 'Accept', value: 'application/json' },
			    	{ name: 'Content-type', value: 'application/json' }
			    ],
			    postData: postData
			}
			registerResult = await pk.util.jsonHttpData(http_options);
			registerResult.pkce = await pk.simple_crypto.createB64Code(48);
		}
		var persona_id = await pk.ptools.locate_or_add_credential_persona(iss, registerResult);
		if (!persona_id){
			throw ('no persona_id located or added in create_persona_with_credential');
		} 
		// await request_credential(persona_id, login_hint, iss, next_step, claims)
		await request_credential(persona_id, login_hint, iss);
	}
	catch(err){
		console.log('ERROR in oidc_initiate_login - ' + error_message, err);
	}
}
async function request_credential(persona_id, login_hint, issuer, next_step, claims)
{
  try {
    error_message = 'Unable to connect to the credential_issuer';
    var metadataString = await pk.token.requestOPMetadata(issuer);
    error_message = 'Unable to parse credential_issuer metadata';
    var credential_issuer_metadata = JSON.parse(metadataString);
    error_message = 'Error getting or creating credential_issuer claims';
    var credential_issuer_claims = await getOrCreateCredentialIssuerClaims(issuer, persona_id); 
    if (credential_issuer_claims){
      var endpoint = credential_issuer_metadata.authorization_endpoint;
      var serviceUrl = pk.sts.selfIssuedIssuerIdentifier();
      var redirect_uri = serviceUrl;
      var nonce = await pk.simple_crypto.randomString();
      var nonceInfo = {
        nonce: nonce,
        persona_id: persona_id
      }
      pk.nonceCache.set('cred_request_nonce_info', JSON.stringify(nonceInfo));
      var scope = 'openid openid_credential';
      var response_type = 'id_token token';
      response_type = 'code';
      var state_params = {
        tok_ept: credential_issuer_metadata.token_endpoint,
        cred_ept: credential_issuer_metadata.credential_endpoint,
        sub: persona_id
      }
      if (next_step){
        state_params.next_step = next_step;
      }
      var state = pk.util.createParameterString(state_params).substring(1);
      var persona = pk.ptools.getPersona('id', persona_id);
      var parameters = {
        client_id: persona.client_id,
        redirect_uri: redirect_uri,
        response_type: response_type,
        nonce: nonce,
        state: state,
        scope: scope,
        login_hint: login_hint,
        code_challenge: await pk.simple_crypto.digestSha256(persona.data.pkce),
        code_challenge_method: 'S256'
      };
      if (claims){
        parameters.claims = claims;
      }
      var parameterString = pk.util.createParameterString(parameters);
      window.location = endpoint + parameterString;
    }
    else{
      alert('Error populating credential_issuer object');
      throw ("no credential_issuer claims in request_credential");
    }
  }
  catch (err){
    var alert_message = error_message + err; 
    if (error_message){
      pk.pmanager.managerNotification(error_message, 'alert-warning', true);
    }
    else{
      pk.pmanager.managerNotification(removeNotification);
    }
  }
  async function getOrCreateCredentialIssuerClaims(issuer, persona_id){
    try {
      var credential_issuer_claims_id = persona_id;
      var credential_issuer_claims = await pk.dbs['wallet'].provider.getDocument(pk.dbs['wallet'], 
        'credential_issuer_claims', credential_issuer_claims_id);
    }
    catch (err){
      // this doesn't exist yet - create it so the response from the
      // credential_issuer will be accepted
      var proposed_credential_issuer_claims = {
        issuer: issuer,
        id: persona_id
      };
      credential_issuer_claims = await pk.dbs['wallet'].provider.createOrUpdateDocument(pk.dbs['wallet'], 
        'credential_issuer_claims', proposed_credential_issuer_claims);
    }
    return credential_issuer_claims;
  }
}