oauth2-mock-server
Version:
Configurable OAuth2/OpenID Connect server for automated testing and development purposes
195 lines (192 loc) • 6.88 kB
JavaScript
import { c as assertIsPlainObject, l as assertIsString, t as OAuth2Server } from "./oauth2-server-BlHEzMWT.mjs";
import { AssertionError } from "node:assert";
import { readFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
//#region src/cli.ts
/**
* Copyright (c) AXA Assistance France
*
* Licensed under the AXA Assistance France License (the "License"); you
* may not use this file except in compliance with the License.
* A copy of the License can be found in the LICENSE.md file distributed
* together with this file.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @module cli */
/**
* Shifts a value from the given array, throwing an error
* if the array is empty or the value is undefined.
* @param arr - The array to shift a value from.
* @returns The shifted value.
*/
function shift(arr) {
if (arr.length === 0) throw new AssertionError({ message: "Empty array" });
const val = arr.shift();
if (val === void 0) throw new AssertionError({ message: "Empty value" });
return val;
}
/**
* Reads a JSON file and parses its content.
* @param filepath - The path to the JSON file.
* @returns The parsed JSON object.
*/
function readJsonFromFile(filepath) {
const content = readFileSync(filepath, "utf8");
const maybeJson = JSON.parse(content);
assertIsPlainObject(maybeJson, `File "${filepath}" doesn't contain a properly JSON serialized object.`);
return maybeJson;
}
const scriptName = "oauth2-mock-server";
const defaultOptions = {
port: 8080,
keys: [],
saveJWK: false,
issuerUrlTrailingSlash: false
};
/**
* Runs the CLI with the given arguments and returns the started server, or
* null when the help option is passed.
* @param args - Command-line arguments to parse.
* @returns The started server, or null if help was requested.
*/
async function cli(args) {
let options;
try {
options = parseCliArgs(args);
} catch (err) {
console.error(err instanceof Error ? err.message : err);
process.exitCode = 1;
throw err;
}
if (options === null) {
showHelp(scriptName);
return null;
}
return await startServer(options);
}
function parseCliArgs(args) {
const opts = {
...defaultOptions,
keys: []
};
while (args.length > 0) {
const arg = shift(args);
switch (arg) {
case "-h":
case "--help": return null;
case "-a":
opts.host = shift(args);
break;
case "-p":
opts.port = parsePort(shift(args));
break;
case "-c":
opts.cert = shift(args);
break;
case "-k":
opts.key = shift(args);
break;
case "--jwk":
opts.keys.push(readJsonFromFile(shift(args)));
break;
case "--save-jwk":
opts.saveJWK = true;
break;
case "--issuer-url-trailing-slash":
opts.issuerUrlTrailingSlash = true;
break;
default: throw new Error(`Unrecognized option '${arg}'.`);
}
}
return opts;
}
function showHelp(scriptName) {
console.log(`Usage: ${scriptName} [options]
${scriptName} -a localhost -p 8080
Options:
-h, --help Shows this help information.
-a <address> Address on which the server will listen for connections.
If omitted, the server will accept connections on [::]
if IPv6 is available, or 0.0.0.0 otherwise.
-p <port> TCP port on which the server will listen for connections.
If omitted, 8080 will be used.
If 0 is provided, the operating system will assign
an arbitrary unused port.
--issuer-url-trailing-slash Adds a trailing slash to the issuer url.
-c <cert> Optional file path to an SSL cert. Both cert and key need
to be supplied to enable SSL.
-k <key> Optional file path to an SSL key. Both key and cert need
to be supplied to enable SSL.
--jwk <filename> Adds a JSON-formatted key to the server's keystore.
Can be specified many times.
--save-jwk Saves all the keys in the keystore as "{kid}.json".
If no keys are added via the --jwk option, a new random RSA key
will be generated. This key can then be saved to disk with the --save-jwk
for later reuse.`);
}
function parsePort(portStr) {
const port = parseInt(portStr, 10);
if (Number.isNaN(port) || port < 0 || port > 65535) throw new Error("Invalid port number.");
return port;
}
async function saveJWK(keys) {
for (const key of keys) {
const filename = `${key.kid}.json`;
await writeFile(filename, JSON.stringify(key, null, 2));
console.log(`JSON web key written to file "${filename}".`);
}
}
async function startServer(opts) {
const server = new OAuth2Server(opts.key, opts.cert, { shouldIssuerUrlBeSuffixedWithATralingSlash: opts.issuerUrlTrailingSlash });
await Promise.all(opts.keys.map(async (key) => {
const jwk = await server.issuer.keys.add(key);
console.log(`Added key with kid "${jwk.kid}"`);
}));
if (opts.keys.length === 0) {
const jwk = await server.issuer.keys.generate("RS256");
console.log(`Generated new RSA key with kid "${jwk.kid}"`);
}
if (opts.saveJWK) await saveJWK(server.issuer.keys.toJSON(true));
await server.start(opts.port, opts.host);
const addr = server.address();
const hostname = addr.family === "IPv6" ? `[${addr.address}]` : addr.address;
console.log(`OAuth 2 server listening on http://${hostname}:${addr.port.toString()}`);
assertIsString(server.issuer.url, "Empty host");
console.log(`OAuth 2 issuer is ${server.issuer.url}`);
process.once("SIGINT", () => {
console.log("OAuth 2 server is stopping...");
async function handler() {
await server.stop();
}
handler().catch((e) => {
throw e;
});
console.log("OAuth 2 server has been stopped.");
});
return server;
}
//#endregion
//#region src/oauth2-mock-server.ts
/**
* Copyright (c) AXA Assistance France
*
* Licensed under the AXA Assistance France License (the "License"); you
* may not use this file except in compliance with the License.
* A copy of the License can be found in the LICENSE.md file distributed
* together with this file.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const _default = cli(process.argv.slice(2));
//#endregion
export { _default as default };