oidc-lib
Version:
A library for creating OIDC Service Providers
954 lines (846 loc) • 23.3 kB
JavaScript
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);
}
}