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
JavaScript
"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;