supertokens-auth-react
Version:
ReactJS SDK that provides login functionality with SuperTokens.
701 lines (691 loc) • 36.3 kB
JavaScript
"use strict";
var genericComponentOverrideContext = require("./genericComponentOverrideContext.js");
var MultiFactorAuthWebJS = require("supertokens-web-js/recipe/multifactorauth");
var utils = require("supertokens-web-js/utils");
var postSuperTokensInitCallbacks = require("supertokens-web-js/utils/postSuperTokensInitCallbacks");
var sessionClaimValidatorStore = require("supertokens-web-js/utils/sessionClaimValidatorStore");
var windowHandler = require("supertokens-web-js/utils/windowHandler");
var index = require("./recipeModule-shared.js");
var types = require("./multifactorauth-shared.js");
function _interopDefault(e) {
return e && e.__esModule ? e : { default: e };
}
var MultiFactorAuthWebJS__default = /*#__PURE__*/ _interopDefault(MultiFactorAuthWebJS);
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
var DEFAULT_FACTOR_CHOOSER_PATH = "/mfa";
var MFA_INFO_CACHE_KEY = "st-mfa-info-cache";
// This is a simple in-memory lock using a promise
// We do not need anything more complex than this, since the cache we are locking is in sessionStorage anyway.
var lockProm = undefined;
var getFunctionOverrides = function (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_onHandleEvent
) {
return function (originalImp) {
return genericComponentOverrideContext.__assign(genericComponentOverrideContext.__assign({}, originalImp), {
resyncSessionAndFetchMFAInfo: function (input) {
return genericComponentOverrideContext.__awaiter(this, void 0, void 0, function () {
var stWindow, stored, parsed, unlock, stored_1, parsed, val;
return genericComponentOverrideContext.__generator(this, function (_a) {
switch (_a.label) {
case 0:
stWindow = windowHandler.WindowHandlerReference.getReferenceOrThrow();
// If someone is refreshing from the server we wait for it to finish.
return [4 /*yield*/, lockProm];
case 1:
// If someone is refreshing from the server we wait for it to finish.
_a.sent();
return [4 /*yield*/, stWindow.windowHandler.sessionStorage.getItem(MFA_INFO_CACHE_KEY)];
case 2:
stored = _a.sent();
if (stored !== null) {
parsed = JSON.parse(stored);
if (parsed.t > Date.now() - 1000) {
return [
2 /*return*/,
genericComponentOverrideContext.__assign(
genericComponentOverrideContext.__assign({}, parsed.v),
{
// Adding a fake response is not great, but we do want to add something and this way it's detectable by the app
// so they could even add specific handling for it if they preferred.
fetchResponse: new Response(null, { status: 304 }),
}
),
];
}
}
_a.label = 3;
case 3:
if (!(lockProm !== undefined)) return [3 /*break*/, 5];
return [4 /*yield*/, lockProm];
case 4:
_a.sent();
return [3 /*break*/, 3];
case 5:
lockProm = new Promise(function (res) {
return (unlock = res);
});
_a.label = 6;
case 6:
_a.trys.push([6, , 11, 12]);
return [4 /*yield*/, stWindow.windowHandler.sessionStorage.getItem(MFA_INFO_CACHE_KEY)];
case 7:
stored_1 = _a.sent();
if (stored_1 !== null) {
parsed = JSON.parse(stored_1);
if (parsed.t > Date.now() - 1000) {
return [
2 /*return*/,
genericComponentOverrideContext.__assign(
genericComponentOverrideContext.__assign({}, parsed.v),
{
// Adding a fake response is not great, but we do want to add something and this way it's detectable by the app
// so they could even add specific handling for it if they preferred.
fetchResponse: new Response(null, { status: 304 }),
}
),
];
}
}
return [4 /*yield*/, originalImp.resyncSessionAndFetchMFAInfo(input)];
case 8:
val = _a.sent();
if (!(val.status === "OK")) return [3 /*break*/, 10];
// We are not storing the fetchResponse
return [
4 /*yield*/,
stWindow.windowHandler.sessionStorage.setItem(
MFA_INFO_CACHE_KEY,
JSON.stringify({
t: Date.now(),
v: {
emails: val.emails,
phoneNumbers: val.phoneNumbers,
factors: val.factors,
status: val.status,
},
})
),
];
case 9:
// We are not storing the fetchResponse
_a.sent();
_a.label = 10;
case 10:
return [2 /*return*/, val];
case 11:
// Release the lock
lockProm = undefined;
unlock();
return [7 /*endfinally*/];
case 12:
return [2 /*return*/];
}
});
});
},
});
};
};
var MultiFactorAuthClaimClass = /** @class */ (function () {
function MultiFactorAuthClaimClass(getRecipe, getRedirectURL, onFailureRedirection) {
var _this = this;
this.webJSClaim = new MultiFactorAuthWebJS.MultiFactorAuthClaimClass(function () {
return getRecipe().webJSRecipe;
});
this.refresh = this.webJSClaim.refresh;
this.getLastFetchedTime = this.webJSClaim.getLastFetchedTime;
this.getValueFromPayload = this.webJSClaim.getValueFromPayload;
this.id = this.webJSClaim.id;
var defaultOnFailureRedirection = function (_a) {
var reason = _a.reason,
userContext = _a.userContext;
return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () {
var recipe, nextFactorOptions, availableFactors, mfaInfo_1, availableFactors;
return genericComponentOverrideContext.__generator(this, function (_b) {
switch (_b.label) {
case 0:
recipe = getRecipe();
nextFactorOptions =
reason.oneOf ||
reason.allOfInAnyOrder ||
(reason.factorId !== undefined ? [reason.factorId] : undefined);
if (!(nextFactorOptions !== undefined)) return [3 /*break*/, 1];
genericComponentOverrideContext.logDebugMessage(
"Redirecting to MFA on next array from validation failure: " +
nextFactorOptions.join(", ")
);
availableFactors = recipe
.getSecondaryFactors(userContext)
.filter(function (v) {
return nextFactorOptions.factors.next.includes(v.id);
})
.map(function (v) {
return v.id;
});
// In this case we got here from a validator that defined the list of validators
if (availableFactors.length === 1) {
return [
2 /*return*/,
getRedirectURL(
{ action: "GO_TO_FACTOR", factorId: availableFactors[0] },
userContext
),
];
} else {
return [
2 /*return*/,
getRedirectURL(
{ action: "FACTOR_CHOOSER", nextFactorOptions: nextFactorOptions },
userContext
),
];
}
case 1:
return [
4 /*yield*/,
recipe.webJSRecipe.resyncSessionAndFetchMFAInfo({ userContext: userContext }),
];
case 2:
mfaInfo_1 = _b.sent();
availableFactors = recipe
.getSecondaryFactors(userContext)
.filter(function (v) {
return mfaInfo_1.factors.next.includes(v.id);
})
.map(function (v) {
return v.id;
});
genericComponentOverrideContext.logDebugMessage(
"Redirecting to MFA on next array from backend: " + availableFactors.join(", ")
);
if (availableFactors.length === 1) {
return [
2 /*return*/,
getRedirectURL(
{ action: "GO_TO_FACTOR", factorId: availableFactors[0] },
userContext
),
];
} else {
return [2 /*return*/, getRedirectURL({ action: "FACTOR_CHOOSER" }, userContext)];
}
case 3:
// If this happens the user can't complete sign-in (the claim validator fails, but there is no valid next factor for us)
// Returning undefined here will make SessionAuth render an access denied screen.
return [2 /*return*/, undefined];
}
});
});
};
this.validators = genericComponentOverrideContext.__assign(
genericComponentOverrideContext.__assign({}, this.webJSClaim.validators),
{
hasCompletedMFARequirementsForAuth: function (doRedirection, showAccessDeniedOnFailure) {
if (doRedirection === void 0) {
doRedirection = true;
}
if (showAccessDeniedOnFailure === void 0) {
showAccessDeniedOnFailure = true;
}
var orig = _this.webJSClaim.validators.hasCompletedMFARequirementsForAuth();
return genericComponentOverrideContext.__assign(
genericComponentOverrideContext.__assign({}, orig),
{
showAccessDeniedOnFailure: showAccessDeniedOnFailure,
onFailureRedirection:
onFailureRedirection !== null && onFailureRedirection !== void 0
? onFailureRedirection
: function (_a) {
var reason = _a.reason,
userContext = _a.userContext;
return doRedirection
? defaultOnFailureRedirection({
reason: reason,
userContext: userContext,
})
: undefined;
},
}
);
},
hasCompletedFactors: function (requirements, doRedirection, showAccessDeniedOnFailure) {
if (doRedirection === void 0) {
doRedirection = true;
}
if (showAccessDeniedOnFailure === void 0) {
showAccessDeniedOnFailure = true;
}
var orig = _this.webJSClaim.validators.hasCompletedFactors(requirements);
return genericComponentOverrideContext.__assign(
genericComponentOverrideContext.__assign({}, orig),
{
showAccessDeniedOnFailure: showAccessDeniedOnFailure,
onFailureRedirection:
onFailureRedirection !== null && onFailureRedirection !== void 0
? onFailureRedirection
: function (_a) {
var reason = _a.reason,
userContext = _a.userContext;
return doRedirection
? defaultOnFailureRedirection({
reason: reason,
userContext: userContext,
})
: undefined;
},
}
);
},
}
);
}
return MultiFactorAuthClaimClass;
})();
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
function normaliseMultiFactorAuthFeature(config) {
var _a;
if (config === undefined) {
config = {};
}
var disableDefaultUI = config.disableDefaultUI === true;
var override = genericComponentOverrideContext.__assign(
{
functions: function (originalImplementation) {
return originalImplementation;
},
},
config.override
);
return genericComponentOverrideContext.__assign(
genericComponentOverrideContext.__assign(
{},
genericComponentOverrideContext.normaliseRecipeModuleConfig(config)
),
{
disableDefaultUI: disableDefaultUI,
firstFactors: config === null || config === void 0 ? void 0 : config.firstFactors,
getSecondaryFactorInfo: function (orig) {
return orig;
},
factorChooserScreen: (_a = config.factorChooserScreen) !== null && _a !== void 0 ? _a : {},
override: override,
}
);
}
function getAvailableFactors(factors, nextArrayQueryParam, recipe, userContext) {
genericComponentOverrideContext.logDebugMessage(
"getAvailableFactors: allowed to setup: ".concat(factors.allowedToSetup)
);
genericComponentOverrideContext.logDebugMessage(
"getAvailableFactors: already setup: ".concat(factors.alreadySetup)
);
genericComponentOverrideContext.logDebugMessage("getAvailableFactors: next from factorInfo: ".concat(factors.next));
genericComponentOverrideContext.logDebugMessage(
"getAvailableFactors: nextArrayQueryParam: ".concat(nextArrayQueryParam)
);
genericComponentOverrideContext.logDebugMessage(
"getAvailableFactors: secondary factors: ".concat(
recipe.getSecondaryFactors(userContext).map(function (f) {
return f.id;
})
)
);
// There are 3 cases here:
// 1. The app provided an array of factors to show (nextArrayQueryParam) -> we show whatever is in the array
// 2. no app provided list and validator passed -> we show all factors available to set up or complete
// 3. no app provided list and validator failing -> we show whatever the BE tells us to (this is already filtered by allowedToSetup&alreadySetup on the BE)
var nextArr = nextArrayQueryParam !== undefined ? nextArrayQueryParam.split(",") : factors.next;
var availableFactors = recipe.getSecondaryFactors(userContext).filter(function (_a) {
var id = _a.id;
return nextArr.length === 0
? factors.allowedToSetup.includes(id) || factors.alreadySetup.includes(id)
: nextArr.includes(id);
});
return availableFactors;
}
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
var MultiFactorAuth = /** @class */ (function (_super) {
genericComponentOverrideContext.__extends(MultiFactorAuth, _super);
function MultiFactorAuth(config, webJSRecipe) {
if (webJSRecipe === void 0) {
webJSRecipe = MultiFactorAuthWebJS__default.default;
}
var _this = _super.call(this, config) || this;
_this.webJSRecipe = webJSRecipe;
_this.recipeID = MultiFactorAuth.RECIPE_ID;
_this.secondaryFactors = [];
_this.getDefaultRedirectionURL = function (context, userContext) {
return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () {
var nParam, redirectInfo;
return genericComponentOverrideContext.__generator(this, function (_b) {
if (context.action === "FACTOR_CHOOSER") {
nParam =
context.nextFactorOptions && context.nextFactorOptions.length > 0
? context.nextFactorOptions.join(",")
: undefined;
return [
2 /*return*/,
genericComponentOverrideContext.getDefaultRedirectionURLForPath(
this.config,
DEFAULT_FACTOR_CHOOSER_PATH,
context,
{
n: nParam,
stepUp: context.stepUp ? "true" : undefined,
}
),
];
} else if (context.action === "GO_TO_FACTOR") {
redirectInfo = this.getSecondaryFactors(userContext).find(function (f) {
return f.id === context.factorId;
});
if (redirectInfo !== undefined) {
return [
2 /*return*/,
genericComponentOverrideContext.getDefaultRedirectionURLForPath(
this.config,
redirectInfo.path,
context,
{
setup: context.forceSetup ? "true" : undefined,
stepUp: context.stepUp ? "true" : undefined,
}
),
];
}
throw new Error("Requested redirect to unknown factor id: " + context.factorId);
} else {
return [2 /*return*/, "/"];
}
});
});
};
postSuperTokensInitCallbacks.PostSuperTokensInitCallbacks.addPostInitCallback(function () {
var defaultFactorsValidator =
MultiFactorAuth.MultiFactorAuthClaim.validators.hasCompletedMFARequirementsForAuth();
sessionClaimValidatorStore.SessionClaimValidatorStore.addClaimValidatorFromOtherRecipe(
defaultFactorsValidator
);
types.Session.getInstanceOrThrow().addEventListener(function () {
// We clear the cache if the session updated, since that may mean that the MFA info has changed
var stWindow = windowHandler.WindowHandlerReference.getReferenceOrThrow();
stWindow.windowHandler.sessionStorage.removeItemSync(MFA_INFO_CACHE_KEY);
});
});
return _this;
}
MultiFactorAuth.init = function (config) {
var normalisedConfig = normaliseMultiFactorAuthFeature(config);
return {
recipeID: MultiFactorAuth.RECIPE_ID,
authReact: function (appInfo) {
MultiFactorAuth.instance = new MultiFactorAuth(
genericComponentOverrideContext.__assign(
genericComponentOverrideContext.__assign({}, normalisedConfig),
{ appInfo: appInfo, recipeId: MultiFactorAuth.RECIPE_ID }
)
);
return MultiFactorAuth.instance;
},
webJS: MultiFactorAuthWebJS__default.default.init(
genericComponentOverrideContext.__assign(
genericComponentOverrideContext.__assign({}, normalisedConfig),
{
override: {
functions: function (originalImpl, builder) {
var functions = getFunctionOverrides(normalisedConfig.onHandleEvent);
builder.override(functions);
builder.override(normalisedConfig.override.functions);
return originalImpl;
},
},
}
)
),
};
};
MultiFactorAuth.getInstance = function () {
return MultiFactorAuth.instance;
};
MultiFactorAuth.getInstanceOrThrow = function () {
if (MultiFactorAuth.instance === undefined) {
var error = "No instance of MultiFactorAuth found. Make sure to call the MultiFactorAuth.init method.";
// eslint-disable-next-line supertokens-auth-react/no-direct-window-object
if (typeof window === "undefined") {
error = error + genericComponentOverrideContext.SSR_ERROR;
}
throw Error(error);
}
return MultiFactorAuth.instance;
};
MultiFactorAuth.prototype.addMFAFactors = function (secondaryFactors) {
this.secondaryFactors = genericComponentOverrideContext.__spreadArray(
genericComponentOverrideContext.__spreadArray(
[],
this.secondaryFactors.filter(function (factor) {
return secondaryFactors.every(function (newFactor) {
return factor.id !== newFactor.id;
});
}),
true
),
secondaryFactors,
true
);
};
MultiFactorAuth.prototype.isFirstFactorEnabledOnClient = function (factorId) {
return this.config.firstFactors === undefined || this.config.firstFactors.includes(factorId);
};
MultiFactorAuth.prototype.getSecondaryFactors = function (userContext) {
return this.config.getSecondaryFactorInfo(this.secondaryFactors, userContext);
};
MultiFactorAuth.prototype.redirectToFactor = function (_b) {
var factorId = _b.factorId,
forceSetup = _b.forceSetup,
stepUp = _b.stepUp,
redirectBack = _b.redirectBack,
navigate = _b.navigate,
userContext = _b.userContext;
return genericComponentOverrideContext.__awaiter(this, void 0, void 0, function () {
var url, redirectUrl, redirectUrl;
return genericComponentOverrideContext.__generator(this, function (_c) {
switch (_c.label) {
case 0:
return [
4 /*yield*/,
this.getRedirectUrl(
{
action: "GO_TO_FACTOR",
forceSetup: forceSetup,
stepUp: stepUp,
factorId: factorId,
tenantIdFromQueryParams:
genericComponentOverrideContext.getTenantIdFromQueryParams(),
},
utils.getNormalisedUserContext(userContext)
),
];
case 1:
url = _c.sent();
if (url === null) {
return [2 /*return*/];
}
// If redirectBack was set to true we always set redirectToPath to that value
// otherwise we try and get it from the query params, finally falling back to not setting it.
// Example:
// 1. If the app calls this on pathX and with redirectBack=false, we redirect to /auth/mfa/factor-id
// 2. If the app calls this on pathX and with redirectBack=true, we redirect to /auth/mfa/factor-id?redirectToPath=pathX
// 3. If:
// - the app redirects to the factor chooser with redirectBack=true from path=X, they end up on /auth/mfa?redirectToPath=pathX
// - the factor chooser screen then calls this with redirectBack=false, then they end up on /auth/mfa/factor-id?redirectToPath=pathX
// 4. In the unlikely case that the app itself uses a `redirectToPath` query param internally
// and is on a custom path that has a redirectToPath set to pathX when calling this function,
// then we keep that in the query params if redirectBack is set to false.
if (redirectBack) {
redirectUrl =
genericComponentOverrideContext.getCurrentNormalisedUrlPathWithQueryParamsAndFragments();
url = genericComponentOverrideContext.appendQueryParamsToURL(url, {
redirectToPath: redirectUrl,
});
} else {
redirectUrl = genericComponentOverrideContext.getRedirectToPathFromURL();
if (redirectUrl) {
url = genericComponentOverrideContext.appendQueryParamsToURL(url, {
redirectToPath: redirectUrl,
});
}
}
return [
2 /*return*/,
genericComponentOverrideContext.SuperTokens.getInstanceOrThrow().redirectToUrl(
url,
navigate
),
];
}
});
});
};
MultiFactorAuth.prototype.redirectToFactorChooser = function (_b) {
var _c = _b.redirectBack,
redirectBack = _c === void 0 ? false : _c,
_d = _b.nextFactorOptions,
nextFactorOptions = _d === void 0 ? [] : _d,
stepUp = _b.stepUp,
navigate = _b.navigate,
userContext = _b.userContext;
return genericComponentOverrideContext.__awaiter(this, void 0, void 0, function () {
var url, redirectUrl, redirectUrl;
return genericComponentOverrideContext.__generator(this, function (_e) {
switch (_e.label) {
case 0:
return [
4 /*yield*/,
this.getRedirectUrl(
{
action: "FACTOR_CHOOSER",
nextFactorOptions: nextFactorOptions,
stepUp: stepUp,
tenantIdFromQueryParams:
genericComponentOverrideContext.getTenantIdFromQueryParams(),
},
utils.getNormalisedUserContext(userContext)
),
];
case 1:
url = _e.sent();
if (url === null) {
return [2 /*return*/];
}
if (redirectBack) {
redirectUrl =
genericComponentOverrideContext.getCurrentNormalisedUrlPathWithQueryParamsAndFragments();
url = genericComponentOverrideContext.appendQueryParamsToURL(url, {
redirectToPath: redirectUrl,
});
} else {
redirectUrl = genericComponentOverrideContext.getRedirectToPathFromURL();
if (redirectUrl) {
url = genericComponentOverrideContext.appendQueryParamsToURL(url, {
redirectToPath: redirectUrl,
});
}
}
return [
2 /*return*/,
genericComponentOverrideContext.SuperTokens.getInstanceOrThrow().redirectToUrl(
url,
navigate
),
];
}
});
});
};
/*
* Tests methods.
*/
MultiFactorAuth.reset = function () {
if (!genericComponentOverrideContext.isTest()) {
return;
}
MultiFactorAuth.instance = undefined;
return;
};
var _a;
_a = MultiFactorAuth;
MultiFactorAuth.RECIPE_ID = "multifactorauth";
MultiFactorAuth.MultiFactorAuthClaim = new MultiFactorAuthClaimClass(
function () {
return MultiFactorAuth.getInstanceOrThrow();
},
function (context, userContext) {
return genericComponentOverrideContext.__awaiter(void 0, void 0, void 0, function () {
return genericComponentOverrideContext.__generator(_a, function (_b) {
switch (_b.label) {
case 0:
return [
4 /*yield*/,
this.getInstanceOrThrow().getRedirectUrl(
genericComponentOverrideContext.__assign(
genericComponentOverrideContext.__assign({}, context),
{
tenantIdFromQueryParams:
genericComponentOverrideContext.getTenantIdFromQueryParams(),
}
),
userContext
),
];
case 1:
return [2 /*return*/, _b.sent() || undefined];
}
});
});
}
);
return MultiFactorAuth;
})(index.RecipeModule);
exports.DEFAULT_FACTOR_CHOOSER_PATH = DEFAULT_FACTOR_CHOOSER_PATH;
exports.MultiFactorAuth = MultiFactorAuth;
exports.getAvailableFactors = getAvailableFactors;