"use strict"; var Keypairs = require("@root/keypairs"); var Keyfetch = require("keyfetch"); var Request = require("@root/request"); var crypto = require("crypto"); var PocketId = module.exports; 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?:\/\//, ""); var emailInfo = await addEmail(req).catch(function (err) { // what to do with this error? console.warn("[pocketid.express.request-email] Error:", err); }); Object.assign(req.user, emailInfo || {}); console.log("authz-user:", req.user); next(); } return _middleware; }; async function addEmail(req) { if (submails[req.user.sid]) { // TODO keep email cached longer than token expiration? return submails[req.user.sid]; } var resp = await Request({ url: req.user.iss + "/api/authz/email", headers: { Authorization: "Bearer " + req.jwt, }, json: true, }); 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(), }; return submails[req.user.sid]; } if (result.unverified_email) { return { unverifiedEmail: result.unverified_email, }; } return null; } 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(); }; };