Emi Matchu
ffcfce2eb8
I'm starting to work with the OpenID Connect stuff in NeoPass, and the library I'm using for discovery doesn't seem to want to do it over a plain `http://` connection. (I dug into the source files, and it just actually is hardcoded to only work with HTTPS, as far as I can tell?) So, I've added logic to `neopass-server` to try to make an HTTPS certificate with the `mkcert` utility (if installed), which is a tool that installs a root certificate authority on your local machine, then helps you create certificates via that authority that will work only on your local machine. I think I'll also be able to remove the "main" server in front of the backing server, because the library I'm using now will be able to "discover" the auth and token endpoints, so it won't matter that our local one uses different URLs than live NeoPass does? We'll find out! I also remove `neopass-server` from the `Procfile`, because I think it's a bit rude to have it auto-try to run `mkcert`. We could like, make the process just a no-op in that case? But I think I'd prefer to just run `neopass-server` by hand when I want it, for simplicity.
144 lines
5.3 KiB
JavaScript
Executable file
144 lines
5.3 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
/**
|
|
* A test NeoPass server! This is a very lean, hacky implementation, designed
|
|
* to just see the basic OAuth interactions Work At All.
|
|
*
|
|
* First, we have a "backing server", which is a `oauth2-mock-server` instance
|
|
* that's easy to spin up and have perform OAuth for us. We give it a hardcoded
|
|
* development-only key, and it just auto-grants permissions!
|
|
*
|
|
* We also have a "main server", which obeys the actual NeoPass API: the
|
|
* backing server isn't configurable with stuff like paths, so we use the main
|
|
* server to proxy from the paths in the NeoPass spec to the paths the backing
|
|
* server uses.
|
|
*/
|
|
|
|
const fs = require("node:fs/promises");
|
|
const pathLib = require("node:path");
|
|
const { spawn } = require("node:child_process");
|
|
const urlLib = require("node:url");
|
|
|
|
const { OAuth2Server } = require("oauth2-mock-server");
|
|
const express = require("express");
|
|
|
|
const certPath = pathLib.join(__dirname, "..", "tmp", "localhost.pem");
|
|
const keyPath = pathLib.join(__dirname, "..", "tmp", "localhost-key.pem");
|
|
|
|
async function fileExists(path) {
|
|
try {
|
|
await fs.stat(path);
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") {
|
|
return false;
|
|
}
|
|
throw error;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function ensureCertsExist() {
|
|
if (!(await fileExists(certPath)) || !(await fileExists(keyPath))) {
|
|
console.log(
|
|
"Using mkcert to create localhost.pem and localhost-key.pem in " +
|
|
"the Rails tmp dir, to serve over HTTPS.",
|
|
);
|
|
|
|
const mkcertProc = spawn("mkcert", [
|
|
"-cert-file",
|
|
certPath,
|
|
"-key-file",
|
|
keyPath,
|
|
"localhost",
|
|
], {stdio: ["ignore", process.stdout, process.stderr]});
|
|
|
|
// Wait for the process to finish, raising an error if it fails.
|
|
await new Promise((resolve, reject) => {
|
|
mkcertProc.on("close", (code) => {
|
|
if (code === 0) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`mkcert returned status ${code}`));
|
|
}
|
|
});
|
|
mkcertProc.on("error", (error) => {
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
async function startBackingServer(port) {
|
|
const server = new OAuth2Server(
|
|
keyPath,
|
|
certPath,
|
|
);
|
|
await server.issuer.keys.add({
|
|
// A key we generated for the NeoPass test server. It's okay for its
|
|
// "secret" info to be here, because it's for development only!
|
|
kid: "neopass-server-DEVLOPMENT-ONLY-NOT-FOR-PRODUCTION",
|
|
p: "50btwsJlPbGLUFnCSBZzddyMX_oRQ8nz4lMrpAd4umPLqMUmS0NbBZNf6DI7s8PkRUxtV8KdvZh3OYWavFnbk55GDG4Y_J_wA4XlHU3d5KIfSNaIdbtVp4CFOq1lovho4sYX_26vcGgYb2Azeg7nz_gDpqNmIdJdKuZxzsrboK8",
|
|
kty: "RSA",
|
|
q: "xha0i9_lbOMQhmmni02Dtpocil26GI7W8xbOFyOvceBCRNf-XOA_-W_Xk9ItJRHnAWM1TML36PN0l864d4QAXbBo64FHu2cjdFKnXJNliJaPcOPAMQB_D8GSylU1gTwSpP_vVe8t232LeF1oBwbOoBIS-6NsOpLmL8Sezv6Fpac",
|
|
d: "WSUNeEd_EyaELK7wqT6GJJK_RfYjaE5h6USe9rD9cd_tQE2PaZWXMyZ4OCk5Z5hdG2ryZY7NYsOI2CPs8HCFBqMoKd0z0A0EgB8Dq2fe_-t5Rm0Zq1ZnI5tnBcZeQmw0hDT98Wg00FA53SSUqfnOgI_VuLvquM6f18_XQOKRRfTcwh1a4teDAH0g0s8FVOS5DANtg71mTdq5fEkWmQMD3qKC6SNrx3WXXHezDs0MWdeFqn9Dg7gssSqB7PnqB-hlC_fHnu4gm9nDqPTMzsJC2i8d3adm0AeORRCulGLe7hU-_TgTbZzgIYCgOK_asaewW-6Qk9qFj-J4djBaKIee-Q",
|
|
e: "AQAB",
|
|
use: "sig",
|
|
qi: "WNiwCcAk2x7e0KvuupL2DNU-JUjLEF9Onee5T9u9ihbgGSDIyP04_96TzCIK3wsY6lct64oOo0Er-z5cf_5eOBPD3n0eEL-JuKIgn0mEKrazJOnGQzlyeZPzk4dUO2J7D42ObopfYsoBIcJx-Y_43a6WORDMGSVCiURmKavTHUU",
|
|
dp: "p1_wj-Npq3VDElpzPQJqeuCrAoaSWhHcm21_hs0VdSbl6_UJ2qwbQnS-kudPx7A8El7WPw4MZHrjxdBIBImvXCzOGw7OrHz_ET2ka0nADUe7BlakGTgDLB7ZzHZSuNe36G5eTbCH7PyYunnPp0UERMEDu2RDdLSuUm7F7FdpDOc",
|
|
alg: "RS256",
|
|
dq: "purLCHKKKM7NRfYRsFiI_H2wPwfroHX8uqokz2rKk_Kc5NX9CNYOEmokBfO9BtenCIxIhX5k2G8NeD5BQrSAenIEdy5g-5FVVtevH1s023vDMyU29hOs_eHnh4d1poiwTUk8q_T3d1S7CZnr5r_drRSN2m1C7biLLwVHrLTceVE",
|
|
n: "svVfGU4NGcfBCmQiIOW5uzg5SAN2CWSIQSstnhqZoCdjy5OoKpKVR8O9TbDvxixrvkFyAav90Q0Xse8iFTcjfCKuqINYiuYMXhCvfBlc_DVVOQca9pMpN03LaDofd5Ll4_BFTtt1nSPahwWU7xDM-Bkkh_TcS2qS4N2xbpEGi0q0ZkrJN4WyiDBC2k9WbK-YHr4Rj4JKypFVSeBIrjxVPmlPzgfqlLGGIB0l92SnJDXDMlkWcCCTyLgqSBM04nkxGDSykq_ei76qCdRd7b10wMBaoS9DeBThAyHpur2LoPdH3gxbcwoWExi-jPlNP1LdKVZD8b95OY3CRyMAAMGdKQ",
|
|
});
|
|
|
|
await server.start(port, "localhost");
|
|
console.log(`Started NeoPass backing server at: ${server.issuer.url}`);
|
|
}
|
|
|
|
async function startMainServer(port) {
|
|
const fetch = (await import("node-fetch")).default;
|
|
|
|
const app = express();
|
|
app.use(express.text({ type: "*/*" }));
|
|
|
|
app.get("/", (req, res) => res.end("NeoPass development server for DTI!"));
|
|
|
|
app.get("/oauth2/auth", (req, res) => {
|
|
const query = urlLib.parse(req.url).query;
|
|
res.redirect(`http://localhost:8686/authorize?${query}`);
|
|
});
|
|
|
|
app.post("/oauth2/token", async (req, res) => {
|
|
try {
|
|
// For POST requests, the HTTP spec doesn't allow a redirect to a
|
|
// POST, so we proxy the request instead.
|
|
const backingRes = await fetch("http://localhost:8686/token", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": req.get("Content-Type"),
|
|
},
|
|
body: req.body,
|
|
});
|
|
if (!backingRes.ok) {
|
|
throw new Error(`backing server returned status ${res.status}`);
|
|
}
|
|
|
|
res.set("Content-Type", backingRes.headers.get("Content-Type"));
|
|
return res.end(await backingRes.text());
|
|
} catch (error) {
|
|
console.error(error);
|
|
return res.end(error.message);
|
|
}
|
|
});
|
|
|
|
await new Promise((resolve) => app.listen(port, resolve));
|
|
console.log(`Started NeoPass main server at: http://localhost:${port}`);
|
|
}
|
|
|
|
async function main() {
|
|
await ensureCertsExist();
|
|
await startBackingServer(8686);
|
|
await startMainServer(8585);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
});
|