UNPKG

oidc-lib

Version:

A library for creating OIDC Service Providers

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