pocketid.js/lib/index.js
2020-12-05 21:24:12 -07:00

250 lines
6.6 KiB
JavaScript

"use strict";
import Keypairs from "@root/keypairs";
import Keyfetch from "keyfetch";
import Request from "@root/request";
import crypto from "crypto";
var PocketId = {};
var submails = {};
var rootKeypair;
var _promise = Promise.resolve();
PocketId._keypair = async function (opts) {
_promise = _promise.then(async function () {
var key = opts.key || process.env.PRIVATE_KEY || (rootKeypair && rootKeypair.private);
var keypair = await Keypairs.parseOrGenerate({ key: key });
if (!key) {
console.warn("Generated random keypair:");
console.warn(keypair.public);
}
if (!rootKeypair) {
rootKeypair = keypair;
}
return keypair;
});
return _promise;
};
// TODO remove regexp one day (for performance)
// { issuers, allow }
PocketId.express = function (opts) {
if (!opts) {
opts = {};
}
if (!opts.issuers) {
opts.issuers = ["beta.pocketid.app", "pocketid.app"];
}
async function _middleware(req, res, next) {
var [bearer, token] = (req.headers.authorization || "").split(" ");
req.jwt = token;
var decoded = await Keyfetch.jwt.verify(token, opts).catch(function (err) {
if (opts.allow) {
next();
return;
}
res.statusCode = 403;
res.json({
error: "invalid authorization: " + err.message,
code: "E_AUTH",
});
});
if (!decoded) {
// error has been handled
return;
}
if (!decoded.claims.jti || !decoded.claims.sub || !decoded.claims.iss) {
res.statusCode = 400;
res.json({
error: "invalid token: missing required claims",
error_code: "E_AUTH",
});
return;
}
var iss = decoded.claims.iss || "";
req.jws = decoded;
req.user = {
jti: decoded.claims.jti,
ppid: decoded.claims.sub + "@" + iss,
};
req.user.sid = req.user.ppid + "#" + req.user.jti;
var scheme = "https:";
var m = iss.match(/(https?:\/\/)?localhost(:|\/|$)/);
if (m) {
scheme = m[1] || "http:";
}
req.user.iss = scheme + "//" + iss.replace(/https?:\/\//, "");
await addEmail(req).catch(function (err) {
// what to do with this error?
console.warn("[pocketid.express.request-email] Error:", err);
});
next();
}
return _middleware;
};
async function addEmail(req) {
if (submails[req.user.sid]) {
// TODO keep email cached longer than token expiration?
Object.assign(req.user, submails[req.user.sid]);
return;
}
var resp = await Request({
url: req.user.iss + "/api/authz/email",
headers: {
Authorization: "Bearer " + req.jwt,
},
});
var result = (resp.body && resp.body.result) || resp.body || {};
console.log("authz-email:", result);
if (result.email && result.verified_at) {
submails[req.user.sid] = {
email: result.email,
verifiedAt: result.verified_at,
createdAt: new Date().toISOString(),
};
}
Object.assign(req.user, submails[req.user.jit] || {});
console.log("authz-user:", req.user);
}
PocketId.refreshToken = function (opts) {
var keypair;
(async function () {
keypair = await PocketId._keypair(opts);
})();
if (!opts.getClaims) {
opts.getClaims = async function (req) {
return {};
};
}
return async function _refreshToken(req, res) {
if (!req.user || !req.user.sid) {
res.statusCode = 400;
res.json({
error: "invalid authorization: no user found",
code: "E_AUTH",
});
return;
}
var claims = await opts.getClaims(req).catch(function (err) {
res.statusCode = 500;
res.json({
error: "invalid claims: " + err.message,
code: "E_CLAIMS",
});
return;
});
if (!claims) {
return;
}
var exp = claims.exp || "15m";
if (!claims.sub) {
claims.sub = req.user.email;
}
if (!claims.jti) {
claims.jti = crypto
.randomBytes(16)
.toString("base64")
.replace(/\//g, "_")
.replace(/\+/g, "-")
.replace(/=/g, "");
}
var iss = opts.issuer || opts.iss;
if (!iss) {
iss = (req.connection.encrypted ? "https:" : "http:") + "//" + req.headers.host;
}
var jwt = await Keypairs.signJwt({
jwk: keypair.private,
iss: iss,
exp: exp,
claims: claims,
});
res.json({
success: true,
result: {
access_token: jwt,
},
});
};
};
PocketId.auth = function (opts) {
var pub = opts.pub || opts.jwk;
var keypair;
(async function () {
if (!pub) {
keypair = await PocketId._keypair(opts);
} else {
keypair = { public: pub };
}
})();
if (!opts.getUser) {
opts.getUser = async function (req) {
return { _test: true };
};
}
return async function _auth(req, res, next) {
var [bearer, token] = (req.headers.authorization || "").split(" ");
req.jwt = token;
var _opts = Object.assign({}, opts, { jwk: keypair.public });
var decoded = await Keyfetch.jwt.verify(token, _opts).catch(function (err) {
if (opts.allow) {
next();
return;
}
res.statusCode = 403;
res.json({
error: "invalid authorization: " + err.message,
code: "E_AUTH",
});
});
if (!decoded) {
// error has been handled
return;
}
req.jws = decoded;
req.user = {
id: decoded.claims.sub,
};
var user = await opts.getUser(req).catch(function (err) {
res.statusCode = 500;
res.json({
error: "invalid user: " + err.message,
code: "E_USER",
});
return;
});
if (!user) {
// error has been handled
return;
}
console.log("user:", Object.assign(req.user, user));
next();
};
};
export default PocketId;