UNPKG

lets-mfa

Version:

Free, secure, and quick way to add MFA to your existing app. No user migrations or re-architecture needed!

234 lines (233 loc) 12.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LetsMFA = void 0; const jose = __importStar(require("jose")); const response_utils_1 = require("./response-utils"); const enroll_utils_1 = require("./enroll-utils"); const request_utils_1 = require("./request-utils"); const lets_mfa_rp_lib_1 = require("lets-mfa-rp-lib"); const crypto_1 = __importDefault(require("crypto")); const auth_utils_1 = require("./auth-utils"); const main_1 = require("./main"); const date_util_1 = require("./date-util"); const os_1 = __importDefault(require("os")); const http_1 = require("./lib/http"); const lets_mfa_rp_lib_2 = require("lets-mfa-rp-lib"); class LetsMFA { constructor(options) { this.options = options; if (!options.keys.publicKey || !options.keys.privateKey) { throw new Error("publicKey and privateKey are required"); } try { if (typeof options.keys.publicKey === "string") { this.publicKey = JSON.parse(options.keys.publicKey); } else { this.publicKey = options.keys.publicKey; } if (typeof options.keys.privateKey === "string") { this.privateKey = JSON.parse(options.keys.privateKey); } else { this.privateKey = options.keys.privateKey; } } catch (err) { throw new Error("Could not JSON parse public or private key. Should be a JWK in JSON string format. Use command 'npx letsmfa generate-keys' to generate valid keys"); } } /** Returns the public key */ getPublicKey() { return this.publicKey; } /** The Enrollment flow allows a user to define their methods of authentication. The EnrollmentResponse * object is returned as a query parameter to the responseUrl provided in the EnrollRequest. * * @param responseUrl The URL to which the user will be redirected after enrollment is complete. * @param nestedJWT A signed JWT from another provider, or self generated. This value will be nested in the JWT returned by LetsMFA. * @param enrollOptions Optional configuration for the enrollment flow. These can be used to configure the user interface and to enforce authentication requirements. * @param validThrough Optional The epoch seconds UTC through which the user has to complete enrollment. If not provided, the request will expire in 5 minutes. * @param accountVault Optional supply an existing account vault to allow the user to update/change their methods of authentication. * @param requestId Optional supply a unique identifier for this request. If not provided, a random UUID will be generated. This may be used to prevent replay attacks. * * returns a URL to which the user should be redirect to begin the enrollment flow */ startEnroll(options) { return __awaiter(this, void 0, void 0, function* () { if (this.options.realm == null) throw new Error("REALM_NOT_SET"); if (options.authPolicy == null) options.authPolicy = this.options.defaultAuthPolicy; if (options.authPolicy == null) options.authPolicy = structuredClone(main_1.DEFAULT_AUTH_POLICY); if (!options.isTestAccount) options.authPolicy.methods.test = undefined; if (options.responseUrl == null) options.responseUrl = this.options.defaultResponseUrl; if (options.responseUrl == null) throw new Error("RESPONSE_URL_NOT_SET"); const enrollRequest = (0, enroll_utils_1.generateEnrollRequest)(this.options.realm, this.publicKey, this.options.domain, options.responseUrl, options.nestedJWT, options.authPolicy, options.accountDisplayName, options.validThrough, options.accountVault, this.options.logoUrl, options.requestId); const signedRequest = yield (0, request_utils_1.signRequest)(enrollRequest, yield jose.importJWK(this.privateKey)); const encryptedRequest = yield (0, request_utils_1.encryptRequest)(this.options.realm, signedRequest); const encodedRequest = Buffer.from(JSON.stringify(encryptedRequest)).toString("base64"); const requestUrl = this.options.realm + "/api/enroll/start?request=" + encodedRequest; const response = yield (0, http_1.HTTP)(requestUrl, { method: "GET", }); return response; }); } handleAuthResponse(responseToken) { return __awaiter(this, void 0, void 0, function* () { const letsMFAConfig = yield (0, lets_mfa_rp_lib_2.retrieveLetsMFAConfiguration)(this.options.realm); const { response } = yield (0, http_1.HTTP)(this.options.realm + "/api/exchange/response?responseToken=" + responseToken, { method: "GET", }); const decryptedResponse = yield (0, response_utils_1.decryptResponse)(response, letsMFAConfig, yield jose.importJWK(this.privateKey)); const authResponse = yield (0, response_utils_1.validateResponseSignature)(decryptedResponse, letsMFAConfig); const tokens = yield (0, lets_mfa_rp_lib_1.validateLetsMFAIdToken)(authResponse.id_token, { publicKeyOrSecret: letsMFAConfig.publicKey, issuer: letsMFAConfig.realm, audience: this.options.domain, // algorithms? // maxTokenAge: "5m", // TODO: make this flow through from the request typ: "JWT", nested: this.options.nestedJWTValidationOptions, }); // validate that the subject is the same in all tokens const firstSubject = tokens[0].sub; if (!firstSubject) throw new Error("SUBJECT_NOT_FOUND"); for (const token of tokens) { if (token.sub !== firstSubject) { throw new Error("SUBJECT_MISMATCH"); } } return Object.assign(Object.assign({}, authResponse), { sub: firstSubject, jwt: tokens[0] }); }); } /** A convenience method for generating a self signed JWT (aka a JWS) * that can be used in the generateEnrollRequest and generateAuthenticateRequest methods. * * Supply the user value, which can be any string. THe value will be used as the subject of the JWT. * * @param user The user value to use as the subject of the JWT * @param expirationEpochSeconds Optional The epoch UTC seconds through which the JWT is valid. If not provided, the JWT will expire in 8 hours. * @param requestId Optional supply a unique identifier for this request. If not provided, a random UUID will be generated. This may be used to prevent replay attacks. */ generateSelfSignedJWT(user, expirationEpochSeconds) { return __awaiter(this, void 0, void 0, function* () { if (user == null) throw new Error("USER_NOT_SET"); const letsMFAConfig = yield (0, lets_mfa_rp_lib_2.retrieveLetsMFAConfiguration)(this.options.realm); const utcTime = (0, date_util_1.dateToUTC)(new Date()).getTime(); let exp = expirationEpochSeconds ? expirationEpochSeconds : Math.floor(utcTime / 1000) + 60 * 60 * 8; const jwt = { iss: "self:" + this.options.domain + ":" + (this.options.clientId || os_1.default.hostname()), aud: this.options.domain, sub: user, exp, iat: Math.floor(Date.now() / 1000), jti: crypto_1.default.randomBytes(16).toString("hex"), nbf: Math.floor(Date.now() / 1000) - 60 * 1, }; const signedJWT = yield new jose.SignJWT(jwt) .setProtectedHeader({ alg: letsMFAConfig.signingAlgorithm, }) .sign(yield jose.importJWK(this.privateKey)); return signedJWT; }); } /** The Authentication flow allows a user to authenticate using the methods they have enrolled. */ startAuthentication(options) { return __awaiter(this, void 0, void 0, function* () { if (this.options.realm == null) throw new Error("REALM_NOT_SET"); const publicKey = yield jose.exportJWK(yield jose.importJWK(this.publicKey)); if (options.authPolicy == null) options.authPolicy = this.options.defaultAuthPolicy; if (options.authPolicy == null) options.authPolicy = structuredClone(main_1.DEFAULT_AUTH_POLICY); if (!options.isTestAccount) options.authPolicy.methods.test = undefined; if (options.responseUrl == null) options.responseUrl = this.options.defaultResponseUrl; if (options.responseUrl == null) throw new Error("RESPONSE_URL_NOT_SET"); const request = (0, auth_utils_1.generateAuthenticationRequest)(this.options.realm, publicKey, this.options.domain, options.accountVault, options.responseUrl, options.nestedJWT, options.authPolicy, options.accountDisplayName, options.validThrough, this.options.logoUrl, options.requestId); const signedRequest = yield (0, request_utils_1.signRequest)(request, yield jose.importJWK(this.privateKey)); if (this.options.realm == null) this.options.realm = "https://auth.letsmfa.com"; const encryptedRequest = yield (0, request_utils_1.encryptRequest)(this.options.realm, signedRequest); const encodedRequest = Buffer.from(JSON.stringify(encryptedRequest)).toString("base64"); const requestUrl = this.options.realm + "/api/auth/start?request=" + encodedRequest; const response = yield (0, http_1.HTTP)(requestUrl, { method: "GET", }); return response; }); } validateJwt(jwt) { return __awaiter(this, void 0, void 0, function* () { const letsMFAConfig = yield (0, lets_mfa_rp_lib_2.retrieveLetsMFAConfiguration)(this.options.realm); const tokens = yield (0, lets_mfa_rp_lib_1.validateLetsMFAIdToken)(jwt, { publicKeyOrSecret: letsMFAConfig.publicKey, issuer: letsMFAConfig.realm, audience: this.options.domain, // algorithms? // maxTokenAge: "5m", // TODO: make this flow through from the request typ: "JWT", nested: this.options.nestedJWTValidationOptions, }); return tokens; }); } getRealm() { return this.options.realm || "https://auth.letsmfa.com"; } } exports.LetsMFA = LetsMFA;