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;
}
}