#!/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); });