254 lines
6.7 KiB
JavaScript
254 lines
6.7 KiB
JavaScript
"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,
|
|
},
|
|
});
|
|
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();
|
|
};
|
|
};
|