UNPKG

oidc-lib

Version:

A library for creating OIDC Service Providers

954 lines (846 loc) 23.3 kB
module.exports = { "init": init, "check": check }; const { v4: uuidv4 } = require('uuid'); const protocolSpecList = { CRED_REQUEST: { name: 'Oidc V2 Credential (InProgress)', url: 'client_api/oidc_protocols.html' }, OIDC_CORE: { name: 'OpenID Connect Core 1.0 (2014)', url: 'https://openid.net/specs/openid-connect-core-1_0.html' }, VC_DATA_MODEL_1: { name: 'Verifiable Credentials Data Model 1.0 (2019)', url: 'https://www.w3.org/TR/vc-data-model/' }, TRYBE_OIDC: { name: 'Trybe OIDC Client Api 0.5', url: 'client_api/trybe_1.0.html' } } // snappy is a compression module used only on node sts // and which crashes browserify so exclude it var snappy; if (typeof window === 'undefined'){ snappy = require('snappyjs'); } const logging_values = { PROTOCOL: 1, DEBUG: 2, DETAIL: 4 } var util_functions = { "backSlash": backSlash, "binaryHttpData": binaryHttpData, // "calculateDidUrl": calculateDidUrl, "compress": compress, "content_module_name": content_module_name, "corsOptions": corsOptions, "createDbScaffold": createDbScaffold, "createParameterString": createParameterString, "parseParameterString": parseParameterString, "db_module": db_module, "forwardSlash": forwardSlash, "fullyDecodeURIComponent": fullyDecodeURIComponent, "generateUrlQrCode": generateUrlQrCode, "get_oidc_config": get_oidc_config, "jsonHttpData": jsonHttpData, "isUnixFilesystem": isUnixFilesystem, "logging_integer": logging_integer, "log_always": log_always, "log_debug": log_debug, "log_detail": log_detail, "log_error": log_error, "log_protocol": log_protocol, "merge_claim_maps": merge_claim_maps, "queryPwaApi": queryPwaApi, "randomToURN": randomToURN, "setElementVisibility": setElementVisibility, "uncompress": uncompress, "usingNode": usingNode, "url": url, "mkDirectoriesInPath": mkDirectoriesInPath, "copyDirectory": copyDirectory, "dataTimeString": dataTimeString, "inbound_wire_content": inbound_wire_content, "outbound_wire_content": outbound_wire_content }; var pk; var db_modules = { indexed_db: require('./dbs/indexed_db'), file_db: require('./dbs/file_db') } function init(global_pk){ pk = global_pk; if (!pk.util){ pk.util = {}; } for (var key in util_functions){ pk.util[key] = util_functions[key]; } } function check(){ /* pk.url_module pk.app pk.uti.config pk.base64url */ } function url(uri){ var stdUrl = new pk.url_module.parse(uri); var urlWithPort = { href: stdUrl.href, protocol: stdUrl.protocol, host: stdUrl.host, hostname: stdUrl.hostname, pathname: stdUrl.pathname, search: stdUrl.search, port: stdUrl.port } if (!stdUrl.port){ if (stdUrl.protocol === "http:"){ urlWithPort.port = 80; } else if (stdUrl.protocol === "https:"){ urlWithPort.port = 443; } } else { urlWithPort.port = parseInt(stdUrl.port); } return urlWithPort; } /* function content_views(reqOrModuleName, contentDirectory) { if (!contentDirectory){ contentDirectory = 'claimer_content'; } var module; if (typeof reqOrModuleName === 'string'){ module = reqOrModuleName; } else{ module = content_module_name(reqOrModuleName); } var newsegment = forwardSlash('\\' + contentDirectory + '\\' + module + '\\views\\'); return pk.app.settings.views.replace(forwardSlash('\\views'), newsegment); } */ function content_module_name(req) { var components = req.path.split('/'); if (components.length < 3){ throw "bad path: wrong number of segments: " + req.path; } var contentModule = components[1]; if (pk.util.config.content_modules[contentModule] === undefined){ throw 'Invalid path/module: ' + contentModule; } if (pk.util.config.content_modules[contentModule].enabled === false){ throw 'Module is disabled: ' + contentModule; } return contentModule; } function logging_integer(text){ var value = 0; var optAr = text.split(/[\s]+/); for (var i=0; i<optAr.length; i++){ var int = logging_values[optAr[i]]; if (int){ value |= int; } } return value; } function log_always(message){ console.log(message); } function log_detail(arg1, arg2){ if (pk.util.config.logging & logging_values.DETAIL){ var details; if (arg2 === undefined){ details = arg1; } else{ console.log(arg1 + ':'); details = arg2; } var str = JSON.stringify(details, null, 4); console.log(str); } } function log_debug(message){ if (pk.util.config.logging & logging_values.DEBUG){ console.log(message); } } function log_error(){ var copy = [].slice.call(arguments); copy = copy.slice(1); console.error('********* ERROR (' + arguments[0] + ') ***********'); if (copy[0]){ console.error.apply(this, copy); } } function log_protocol(title, log, arg1, arg2){ var contentParam; var specificationString = ''; if (!arg2){ contentParam = arg1; } else{ contentParam = arg2; var specArray = []; if (Array.isArray(arg1)){ for (var i=0; i< arg1.length; i++){ specArray.push(expandSpecParam(arg1[i])); } } else{ specArray.push(expandSpecParam(arg1)); } for (var i=0; i<specArray.length; i++){ var specParam = specArray[i]; specificationString += 'Specification: [' + specParam.name + '](' + specParam.url + ') \r\n'; } } if (pk.util.config.logging & logging_values.PROTOCOL){ var objectArray; var protocolContent = ''; if (typeof contentParam === 'string'){ protocolContent = contentParam; } else{ if (!Array.isArray(contentParam)){ protocolContent += JSON.stringify(contentParam, null, 2); } else{ var sep = ''; for (var i=0; i< contentParam.length; i++){ protocolContent += contentParam[i].title ? sep + contentParam[i].title + ':\r\n' : ''; var innerContent = contentParam[i].value; protocolContent += (typeof innerContent === 'string' ? innerContent : JSON.stringify(contentParam[i].value, null, 2)) + '\r\n'; sep = '\r\n'; } } } if (usingNode()){ var protocolPath = pk.path.join(process.cwd(), 'SIOP'); mkDirectoriesInPath(protocolPath); var protocolFilePath = pk.path.join(process.cwd(), 'SIOP', 'protocol.txt'); pk.fs.writeFileSync(protocolFilePath, '### [' + log + '] ' + title + '\r\n' + specificationString + dataTimeString() + '\r\n```javascript=1\r\n' + protocolContent + '\r\n```\r\n', { encoding: "utf8", flag: "a+", mode: 0o666 }); } } function expandSpecParam(inputSpec){ var specParam = inputSpec; var regex = /\{(\w+)\}/g; var match = regex.exec(specParam.name); if (match && match[1]){ var matchSpec = protocolSpecList[match[1]]; specParam = { name: matchSpec.name, url: matchSpec.url } if (!specParam.url.startsWith('https:')){ if (!pk.sts.isSelfIssuedSts() && pk.util.httpsServerUrls['CLIENT_API']){ specParam.url = pk.util.httpsServerUrls['CLIENT_API'].href + specParam.url; } else{ specParam.url = 'https://url_not_available/' + specParam.url; } } specParam.url += inputSpec.url; } return specParam; } } /* function calculateDidUrl(did, options){ var didUrl = did; if (didUrl === undefined){ if (options !== undefined && options.defaultLoginRedirect !== undefined){ didUrl = options.defaultLoginRedirect; } else{ didUrl = httpsServerUrl.protocol + '//' + httpsServerUrl.host + pk.util.WALLET_ENDPOINT + '?'; } } else{ if (didUrl !== 'web+openid'){ if (!didUrl.startsWith('https://')){ if (didUrl.startsWith('http://')){ didUrl = didUrl.substring(7); } didUrl = 'https://' + didUrl; } didUrl += pk.util.WALLET_ENDPOINT; } } return didUrl; } */ // forwardSlash is pk.util.slash_normalize - adjusts file paths written // for windows so they work on unix as well. function forwardSlash(windows_path){ if (isUnixFilesystem()){ windows_path = windows_path.replace(/\\/g, '/');; } return windows_path; } // backSlash is slash_denormalize - adjusts file paths written // for unix so they work on windows as well. function backSlash(unix_path){ if (!isUnixFilesystem()){ unix_path = unix_path.replace(/\//g, '\\');; } return unix_path; } // node can run under linux or windows function usingNode(){ return (typeof window === "undefined"); } // unix file system only possible running node function isUnixFilesystem(){ // node rather than browser && /usr/bin in the path return usingNode() && process.env.PATH.indexOf("/usr/bin") >= 0; } function createParameterString(obj, encode){ var handleEncode; if (encode === false){ handleEncode = function(input){ return input; } } else{ handleEncode = encodeURIComponent; } var result = ''; var separator = '?'; for (var key in obj){ var value = obj[key] ? handleEncode(obj[key]) : ''; if (typeof value !== 'string'){ value = pk.base64url.encode(JSON.stringify(value)); } result += separator + handleEncode(key) + '=' + value; separator = '&'; } return result; } function parseParameterString(inputString){ if (inputString.startsWith("?") || inputString.startsWith("#")){ inputString = inputString.substring(1); } var params = inputString.split("&"); var qParams = {}; for (var i=0; i < params.length; i++){ var qParam = params[i].split("="); var key = decodeURIComponent(qParam[0]); var value = fullyDecodeURIComponent(qParam[1]); qParams[key] = value; } return qParams; } function setElementVisibility(id, value) { var element = document.getElementById(id); if (element){ if (value === 'toggle'){ if (element.classList.contains('clms_0')){ value = true; } else{ value = false; } } if (value){ element.classList.remove('clms_0'); element.classList.add('clms_1'); } else{ element.classList.remove('clms_1'); element.classList.add('clms_0'); } } } function fullyDecodeURIComponent(value){ var cutoff = 0; var value1 = decodeURIComponent(value); while (value1 !== value && cutoff < 5){ cutoff++; value = value1; value1 = decodeURIComponent(value); } return value; } async function generateUrlQrCode(uri){ var QRCode = require('qrcode'); return await QRCode.toDataURL(uri); } function get_oidc_config(caller, selector, value){ if (value === true || value === false){ match = value === (caller.oidc_config[selector] !== undefined); } else { // otherwise check for equality match = caller.oidc_config[selector] === value; } if (match){ caller.oidc_config.config_selected = true; return caller.oidc_config; } return null; } // options work as follows // url: '/tester/configuration', // method: 'POST', // headers: [ // { name: 'Content-Type', value: 'application/json'}, // { name: 'Content-Type', value: 'text/html; charset=utf-8'), // { name: 'Accept', value: 'application/json'} // ] function jsonHttpData(options){ return new Promise((resolve, reject) => { if (typeof window === 'undefined'){ // url var rOptions = { url: options.url }; // method var method = options.method; if (method === undefined){ method = 'GET'; } rOptions.method = method.toUpperCase(); // headers if (options.headers !== undefined){ var headers = {}; for (var i=0; i < options.headers.length; i++){ var header = options.headers[i]; headers[header.name] = header.value; } rOptions.headers = headers; } var postData = options.postData; if (postData){ if (typeof postData !== 'string'){ postData = JSON.stringify(postData); } rOptions.body = postData; } var request = pk.request; request(rOptions, function (error, response, body) { if (error){ reject(error); } else if (!body){ reject(response); } else{ if (options.parseJsonResponse){ try{ body = JSON.parse(body); } catch(err){ reject("Error parsing jsonResponse to " + options.url); return; } } resolve(body); } return; }); } else{ var xhr = new XMLHttpRequest(); var method = options.method; if (method === undefined){ method = 'GET'; } xhr.open(method, options.url, true); var authorizationHeaderPresent = false; if (options.headers !== undefined){ for (var i=0; i < options.headers.length; i++){ var header = options.headers[i]; if (header.name === 'Authorization'){ authorizationHeaderPresent = true; } xhr.setRequestHeader(header.name, header.value); } } xhr.onreadystatechange = function () { if (xhr.readyState === 4){ if (xhr.status === 200 || xhr.status === 201) { var response = xhr.responseText; if (options.parseJsonResponse){ try{ response = JSON.parse(response); } catch(err){ reject("Error parsing jsonResponse to " + options.url); return; } } resolve(response); } else if (xhr.status !== 0) { reject(xhr.responseText); } } }; xhr.onerror = function () { console.log('A network error occurred'); // reject('NETWORK ERROR'); } if (authorizationHeaderPresent){ xhr.withCredentials = true; } if (method.toUpperCase() === 'GET' || options.postData === undefined){ xhr.send(); } else{ var postData = options.postData; if (typeof postData !== 'string'){ postData = JSON.stringify(postData); } try{ xhr.send(postData); } catch(err){ console.log('send error: ', err); reject(err); } } } }); } function binaryHttpData(options){ return new Promise((resolve, reject) => { if (typeof window === 'undefined'){ // url var rOptions = { url: options.url }; // method var method = options.method; if (method === undefined){ method = 'GET'; } rOptions.method = method.toUpperCase(); // headers if (options.headers !== undefined){ var headers = {}; for (var i=0; i < options.headers.length; i++){ var header = options.headers[i]; headers[header.name] = header.value; } rOptions.headers = headers; } var postData = options.postData; if (postData){ if (typeof postData !== 'string'){ thow("post binary not yet implemented"); // postData = JSON.stringify(postData); } rOptions.body = postData; } var request = pk.request; request(rOptions, function (error, response, body) { if (error || !body){ reject(response); return; } resolve(body); }); } else{ var xhr = new XMLHttpRequest(); var method = options.method; if (method === undefined){ method = 'GET'; } xhr.open(method, options.url, true); if (options.headers !== undefined){ for (var i=0; i < options.headers.length; i++){ var header = options.headers[i]; xhr.setRequestHeader(header.name, header.value); } } xhr.overrideMimeType('text/plain; charset=x-user-defined'); xhr.onreadystatechange = function () { if (xhr.readyState === 4){ if (xhr.status === 200) { resolve(xhr.responseText); } else { reject(xhr.responseText); } } }; if (method.toUpperCase() === 'GET' || options.postData === undefined){ xhr.send(); } else{ var postData = options.postData; if (typeof postData !== 'string'){ throw("post binary not uet implemented"); // postData = JSON.stringify(postData); } xhr.send(postData); } } }); } function db_module(module){ return db_modules(module); } function createDbScaffold(){ var dbScaffold = {}; if (pk.util.config.sts.db !== undefined){ var db_info = pk.util.config.sts.db; if (db_info === undefined){ console.error('if sts db is used, the provider must be defined'); return; } if (db_modules[db_info.provider] === undefined){ console.error('*** Error: claimer_sts module should have "required(' + db_info['provider'] + ')"'); return; } var _constructor = db_modules[db_info.provider]._constructor; if (_constructor){ dbScaffold['sts'] = new db_modules[db_info.provider][_constructor](); } else{ dbScaffold['sts'] = db_modules[db_info.provider]; } } for (var contentModuleName in pk.util.config.content_modules){ if (pk.util.config.content_modules[contentModuleName].db !== undefined){ var db_info = pk.util.config.content_modules[contentModuleName].db; if (db_info['provider'] === undefined){ continue; } if (db_modules[db_info.provider] === undefined){ console.error("*** Error: contentModule " + contentModuleName + ' should have "required(' + db_info['provider'] + ')"'); continue; } var _constructor = db_modules[db_info.provider]._constructor; if (_constructor){ dbScaffold[contentModuleName] = new db_modules[db_info.provider][_constructor](); } else{ dbScaffold[contentModuleName] = db_modules[db_info.provider]; } } } return dbScaffold; } function randomToURN(base64){ var uuid; if (base64){ var kidArray = new Uint8Array(Buffer.from(base64, 'base64')); var v4Options = { random: kidArray }; uuid = uuidv4(v4Options); } else{ uuid = uuidv4(); } return 'urn:uuid:' + uuid; } function corsOptions(){ var corsOptions = { origin: function (origin, callback) { var whitelist = []; if (whitelist.indexOf(origin) === -1) { callback(null, origin) } else { callback(new Error('Not allowed by CORS')) } } } return pk.cors(corsOptions); } function merge_claim_maps(mapArray){ var merged_scope_claim_map = {}; for (var i=0;i<mapArray.length;i++){ var map = mapArray[i]; if (map){ for (var key in map){ merged_scope_claim_map[key] = map[key]; } } } return merged_scope_claim_map; } function mkDirectoriesInPath(target) { var sep; var firstSeg = true; if (isUnixFilesystem()){ target = forwardSlash(target); sep = '/'; } else{ target = backSlash(target); sep = '\\'; } var segments = target.split(sep); var path = ''; var prefix = ''; for (var i=0; i < segments.length; i++){ path += prefix + segments[i]; prefix = sep; if (firstSeg){ firstSeg = false; continue; } prefix = sep; if (!pk.fs.existsSync(path)){ pk.fs.mkdirSync(path); } } } function copyDirectory(sourceDirName, destDirName){ mkDirectoriesInPath(destDirName); var dirents = pk.fs.readdirSync(sourceDirName, {withFileTypes: true}); for (var i=0; i<dirents.length; i++){ var dirent = dirents[i]; if (dirent.isFile()){ var name = dirent.name var srcPath = pk.path.join(sourceDirName, name); var destPath = pk.path.join(destDirName, name); var buffer = pk.fs.readFileSync(srcPath); pk.fs.writeFileSync(destPath, buffer); } } for (var i=0; i<dirents.length; i++){ var dirent = dirents[i]; if (dirent.isDirectory()){ copyDirectory(pk.path.join(sourceDirName, dirent.name), pk.path.join(destDirName, dirent.name)); } } } function dataTimeString(d){ if (!d){ d = new Date(); } return d.getFullYear() + '-' + ("0" + (d.getMonth() + 1)).slice(-2) + '-' + ("0" + d.getDate()).slice(-2) + ' ' + ("0" + d.getHours()).slice(-2) + ':' + ("0" + d.getMinutes()).slice(-2) + ':' + ("0" + d.getSeconds()).slice(-2) + ':' + ("0" + d.getMilliseconds()).slice(-3) } function inbound_wire_content(req){ var headersOfInterest = [ 'host', 'authorization', "dpop", "credential_sub_dpop", "content-type", 'cache-control', 'pragma' ]; var dispString = req.method + ' ' + req.path + ' HTTP/' + req.httpVersion + '\r\n'; for (var header in req.headers){ if (headersOfInterest.indexOf(header.toLowerCase()) >= 0){ dispString += header + ': ' + req.headers[header] + '\r\n'; } } if (req.method === 'GET'){ var qi = req.originalUrl.indexOf('?'); var paramArr = req.originalUrl.substr(qi + 1).split('&'); var sep = ''; for (var i=0; i<paramArr.length; i++){ var param = paramArr[i]; dispString += sep + decodeURIComponent(param) + '\r\n'; sep = '&'; } } else if (req.method === 'POST'){ if (typeof req.body === 'object'){ dispString += JSON.stringify(req.body, null, 2); } else{ dispString += req.body; } } return(dispString); } function outbound_wire_content(res, body){ var headersToSuppress = [ 'x-powered-by' ]; var httpVersion = '1.1'; if (res.req){ httpVersion = res.req.httpVersion; } var dispString = 'HTTP/' + httpVersion + ' ' + res.statusCode + ' ' + res.statusMessage + '\r\n'; var headers = res.getHeaders(); if (headers.location){ var location = headers.location; var qsOffset = location.indexOf('?'); var url = location.substr(0, qsOffset++); var qs = location.substr(qsOffset); var segs = qs.split('&'); dispString += 'location: ' + url + '?' + '\r\n'; for (var i=0; i < segs.length; i++){ dispString += ' ' + (i ? '&' : '') + segs[i] + '\r\n'; } } for (var header in headers){ if (header === 'location' || headersToSuppress.indexOf(header.toLowerCase()) >= 0){ continue; } dispString += header + ': ' + headers[header] + '\r\n'; } if (body){ if (typeof body === 'object'){ dispString += JSON.stringify(body, null, 2); } else{ dispString += body; } } return(dispString); } function compress(toCompress){ if (!usingNode){ throw 'compress only impemented under node'; } return new Promise((resolve, reject) => { snappy.compress(toCompress, function(err, compressed){ if (err){ reject(err); } resolve(compressed); }); }); } function uncompress(compressed, options){ if (!usingNode){ throw 'compress only impemented under node'; } return new Promise((resolve, reject) => { snappy.uncompress(compressed, options, function(err, result){ if (err){ reject(err); } resolve(result); }); }); }; async function queryPwaApi(url, payload){ try{ var options = { url: url, parseJsonResponse: true, method: 'POST', // headers: [ { name: 'Content-type', value: 'application/x-www-form-urlencoded' } ], headers: [ { name: 'Content-type', value: 'application/json' } ], postData: JSON.stringify(payload) }; var result = await pk.util.jsonHttpData(options); return result; } catch(err){ pk.util.log_error('queryPwaApi', err); } }