Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mfa multitenancy #841

Merged
merged 19 commits into from
Oct 27, 2023
3 changes: 2 additions & 1 deletion coreDriverInterfaceSupported.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"2.20",
"2.21",
"3.0",
"4.0"
"4.0",
"4.1"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public void testPaidStatsIsSentForAllAppsInMultitenancy() throws Exception {
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
new TotpConfig(false), new MfaConfig(null, null),
config
), false);

Expand All @@ -86,6 +87,7 @@ public void testPaidStatsIsSentForAllAppsInMultitenancy() throws Exception {
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
new TotpConfig(false), new MfaConfig(null, null),
config
), false);

Expand All @@ -94,6 +96,7 @@ public void testPaidStatsIsSentForAllAppsInMultitenancy() throws Exception {
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
new TotpConfig(false), new MfaConfig(null, null),
config
), false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public TenantConfig map(ResultSet result) throws StorageQueryException {
new EmailPasswordConfig(result.getBoolean("email_password_enabled")),
new ThirdPartyConfig(result.getBoolean("third_party_enabled"), this.providers),
new PasswordlessConfig(result.getBoolean("passwordless_enabled")),
new TotpConfig(false), // TODO
new MfaConfig(new String[0], new String[0]), // TODO
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
JsonUtils.stringToJsonObject(result.getString("core_config"))
);
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ public static void init(Main main) throws StorageQueryException, IOException {
new TenantConfig(
new TenantIdentifier(null, null, null),
new EmailPasswordConfig(true), new ThirdPartyConfig(true, null),
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
new PasswordlessConfig(true), new JsonObject()), false, false, false);
new PasswordlessConfig(true), new TotpConfig(false), new MfaConfig(null, null),
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
new JsonObject()), false, false, false);
// Not force reloading all resources here (the last boolean in the function above)
// because the ucl for the FeatureFlag is not yet loaded and results in an empty
// instance of eeFeatureFlag. This is applicable only when the core is starting on
Expand All @@ -95,6 +96,8 @@ private TenantConfig[] getAllTenantsFromDb() throws StorageQueryException {
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
new TotpConfig(false),
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
new MfaConfig(null, null),
new JsonObject()
)
};
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/supertokens/utils/SemVer.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class SemVer implements Comparable<SemVer> {
public static final SemVer v2_21 = new SemVer("2.21");
public static final SemVer v3_0 = new SemVer("3.0");
public static final SemVer v4_0 = new SemVer("4.0");
public static final SemVer v4_1 = new SemVer("4.1");

final private String version;

Expand Down
19 changes: 19 additions & 0 deletions src/main/java/io/supertokens/webserver/InputParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,25 @@ public static String parseStringOrThrowError(JsonObject element, String fieldNam
}
}

public static String[] parseStringArrayOrThrowError(JsonObject element, String fieldName, boolean nullable)
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
throws ServletException {
try {
if (nullable && element.get(fieldName) == null) {
return null;
}
JsonArray strings = element.get(fieldName).getAsJsonArray();
String[] result = new String[strings.size()];
for (int i = 0; i < strings.size(); i++) {
result[i] = strings.get(i).getAsString();
}

return result;
} catch (Exception e) {
throw new ServletException(
new WebserverAPI.BadRequestException("Field name '" + fieldName + "' is invalid in JSON input"));
}
}

public static String parseStringFromElementOrThrowError(JsonElement element, String parentFieldName,
boolean nullable) throws ServletException {
try {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/io/supertokens/webserver/WebserverAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ public abstract class WebserverAPI extends HttpServlet {
supportedVersions.add(SemVer.v2_21);
supportedVersions.add(SemVer.v3_0);
supportedVersions.add(SemVer.v4_0);
supportedVersions.add(SemVer.v4_1);
}

public static SemVer getLatestCDIVersion() {
return SemVer.v4_0;
return SemVer.v4_1;
}

public SemVer getLatestCDIVersionForRequest(HttpServletRequest req)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ public BaseCreateOrUpdate(Main main) {

protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdentifier,
TenantIdentifier targetTenantIdentifier, Boolean emailPasswordEnabled,
Boolean thirdPartyEnabled, Boolean passwordlessEnabled, JsonObject coreConfig,
HttpServletResponse resp)
Boolean thirdPartyEnabled, Boolean passwordlessEnabled, Boolean totpEnabled,
boolean hasFirstFactors, String[] firstFactors,
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
boolean hasDefaultRequiredFactorIds, String[] defaultRequiredFactorIds,
JsonObject coreConfig, HttpServletResponse resp)
throws ServletException, IOException {

TenantConfig tenantConfig = Multitenancy.getTenantInfo(main,
Expand All @@ -63,6 +65,8 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
new TotpConfig(false),
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
new MfaConfig(null, null),
new JsonObject()
);
} else {
Expand All @@ -72,6 +76,8 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent
new EmailPasswordConfig(false),
new ThirdPartyConfig(false, null),
new PasswordlessConfig(false),
new TotpConfig(false),
new MfaConfig(null, null),
new JsonObject()
);
}
Expand All @@ -84,6 +90,8 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent
new EmailPasswordConfig(emailPasswordEnabled),
tenantConfig.thirdPartyConfig,
tenantConfig.passwordlessConfig,
tenantConfig.totpConfig,
tenantConfig.mfaConfig,
tenantConfig.coreConfig
);
}
Expand All @@ -94,6 +102,8 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent
tenantConfig.emailPasswordConfig,
new ThirdPartyConfig(thirdPartyEnabled, tenantConfig.thirdPartyConfig.providers),
tenantConfig.passwordlessConfig,
tenantConfig.totpConfig,
tenantConfig.mfaConfig,
tenantConfig.coreConfig
);
}
Expand All @@ -104,6 +114,44 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent
tenantConfig.emailPasswordConfig,
tenantConfig.thirdPartyConfig,
new PasswordlessConfig(passwordlessEnabled),
tenantConfig.totpConfig,
tenantConfig.mfaConfig,
tenantConfig.coreConfig
);
}

if (totpEnabled != null) {
tenantConfig = new TenantConfig(
tenantConfig.tenantIdentifier,
tenantConfig.emailPasswordConfig,
tenantConfig.thirdPartyConfig,
tenantConfig.passwordlessConfig,
new TotpConfig(totpEnabled),
tenantConfig.mfaConfig,
tenantConfig.coreConfig
);
}

if (hasFirstFactors) {
tenantConfig = new TenantConfig(
tenantConfig.tenantIdentifier,
tenantConfig.emailPasswordConfig,
tenantConfig.thirdPartyConfig,
tenantConfig.passwordlessConfig,
tenantConfig.totpConfig,
new MfaConfig(firstFactors, tenantConfig.mfaConfig.defaultRequiredFactorIds),
tenantConfig.coreConfig
);
}

if (hasDefaultRequiredFactorIds) {
tenantConfig = new TenantConfig(
tenantConfig.tenantIdentifier,
tenantConfig.emailPasswordConfig,
tenantConfig.thirdPartyConfig,
tenantConfig.passwordlessConfig,
tenantConfig.totpConfig,
new MfaConfig(tenantConfig.mfaConfig.firstFactors, defaultRequiredFactorIds),
tenantConfig.coreConfig
);
}
Expand All @@ -115,6 +163,8 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent
tenantConfig.emailPasswordConfig,
tenantConfig.thirdPartyConfig,
tenantConfig.passwordlessConfig,
tenantConfig.totpConfig,
tenantConfig.mfaConfig,
coreConfig
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@

import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.utils.SemVer;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.Utils;
import io.supertokens.webserver.api.multitenancy.BaseCreateOrUpdate;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
Expand Down Expand Up @@ -56,6 +55,24 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO
Boolean passwordlessEnabled = InputParser.parseBooleanOrThrowError(input, "passwordlessEnabled", true);
JsonObject coreConfig = InputParser.parseJsonObjectOrThrowError(input, "coreConfig", true);

Boolean totpEnabled = null;
String[] firstFactors = null;
boolean hasFirstFactors = false;
String[] defaultRequiredFactorIds = null;
boolean hasDefaultRequiredFactorIds = false;

if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_1)) {
totpEnabled = InputParser.parseBooleanOrThrowError(input, "totpEnabled", true);
hasFirstFactors = input.has("firstFactors");
if (hasFirstFactors && !input.get("firstFactors").isJsonNull()) {
firstFactors = InputParser.parseStringArrayOrThrowError(input, "firstFactors", true);
}
hasDefaultRequiredFactorIds = input.has("defaultRequiredFactorIds");
if (hasDefaultRequiredFactorIds && !input.get("defaultRequiredFactorIds").isJsonNull()) {
defaultRequiredFactorIds = InputParser.parseStringArrayOrThrowError(input, "defaultRequiredFactorIds", true);
}
}

TenantIdentifier sourceTenantIdentifier;
try {
sourceTenantIdentifier = this.getTenantIdentifierWithStorageFromRequest(req);
Expand All @@ -66,7 +83,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO
super.handle(
req, sourceTenantIdentifier,
new TenantIdentifier(sourceTenantIdentifier.getConnectionUriDomain(), appId, null),
emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, coreConfig, resp);
emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled,
totpEnabled, hasFirstFactors, firstFactors, hasDefaultRequiredFactorIds, defaultRequiredFactorIds,
coreConfig, resp);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.supertokens.Main;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.utils.SemVer;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.Utils;
import jakarta.servlet.ServletException;
Expand Down Expand Up @@ -54,6 +55,24 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO
Boolean passwordlessEnabled = InputParser.parseBooleanOrThrowError(input, "passwordlessEnabled", true);
JsonObject coreConfig = InputParser.parseJsonObjectOrThrowError(input, "coreConfig", true);

Boolean totpEnabled = null;
String[] firstFactors = null;
boolean hasFirstFactors = false;
String[] defaultRequiredFactorIds = null;
boolean hasDefaultRequiredFactorIds = false;

if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_1)) {
totpEnabled = InputParser.parseBooleanOrThrowError(input, "totpEnabled", true);
hasFirstFactors = input.has("firstFactors");
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
if (hasFirstFactors && !input.get("firstFactors").isJsonNull()) {
firstFactors = InputParser.parseStringArrayOrThrowError(input, "firstFactors", true);
}
hasDefaultRequiredFactorIds = input.has("defaultRequiredFactorIds");
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
if (hasDefaultRequiredFactorIds && !input.get("defaultRequiredFactorIds").isJsonNull()) {
defaultRequiredFactorIds = InputParser.parseStringArrayOrThrowError(input, "defaultRequiredFactorIds", true);
}
}

TenantIdentifier sourceTenantIdentifier;
try {
sourceTenantIdentifier = this.getTenantIdentifierWithStorageFromRequest(req);
Expand All @@ -64,7 +83,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO
super.handle(
req, sourceTenantIdentifier,
new TenantIdentifier(connectionUriDomain, null, null),
emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, coreConfig, resp);
emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled,
totpEnabled, hasFirstFactors, firstFactors, hasDefaultRequiredFactorIds, defaultRequiredFactorIds,
coreConfig, resp);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.pluginInterface.multitenancy.*;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.utils.SemVer;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.Utils;
import jakarta.servlet.ServletException;
Expand Down Expand Up @@ -57,6 +58,24 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO
Boolean passwordlessEnabled = InputParser.parseBooleanOrThrowError(input, "passwordlessEnabled", true);
JsonObject coreConfig = InputParser.parseJsonObjectOrThrowError(input, "coreConfig", true);

Boolean totpEnabled = null;
String[] firstFactors = null;
boolean hasFirstFactors = false;
String[] defaultRequiredFactorIds = null;
boolean hasDefaultRequiredFactorIds = false;

if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_1)) {
totpEnabled = InputParser.parseBooleanOrThrowError(input, "totpEnabled", true);
hasFirstFactors = input.has("firstFactors");
if (hasFirstFactors && !input.get("firstFactors").isJsonNull()) {
firstFactors = InputParser.parseStringArrayOrThrowError(input, "firstFactors", true);
}
hasDefaultRequiredFactorIds = input.has("defaultRequiredFactorIds");
if (hasDefaultRequiredFactorIds && !input.get("defaultRequiredFactorIds").isJsonNull()) {
defaultRequiredFactorIds = InputParser.parseStringArrayOrThrowError(input, "defaultRequiredFactorIds", true);
}
}

TenantIdentifier sourceTenantIdentifier;
try {
sourceTenantIdentifier = this.getTenantIdentifierWithStorageFromRequest(req);
Expand All @@ -67,8 +86,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO
super.handle(
req, sourceTenantIdentifier,
new TenantIdentifier(sourceTenantIdentifier.getConnectionUriDomain(), sourceTenantIdentifier.getAppId(), tenantId),
emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, coreConfig, resp);

emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled,
totpEnabled, hasFirstFactors, firstFactors, hasDefaultRequiredFactorIds, defaultRequiredFactorIds,
coreConfig, resp);
}

@Override
Expand All @@ -83,6 +103,11 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
JsonObject result = config.toJson(shouldProtect, tenantIdentifier.getStorage(), CoreConfig.PROTECTED_CONFIGS);
result.addProperty("status", "OK");

if (getVersionFromRequest(req).lesserThan(SemVer.v4_1)) {
result.remove("totp");
result.remove("mfa");
}

super.sendJsonResponse(200, result, resp);
} catch (TenantOrAppNotFoundException e) {
JsonObject result = new JsonObject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO
tenantConfig.thirdPartyConfig.enabled,
newProviders.toArray(new ThirdPartyConfig.Provider[0])),
tenantConfig.passwordlessConfig,
tenantConfig.totpConfig,
tenantConfig.mfaConfig,
tenantConfig.coreConfig);

Multitenancy.addNewOrUpdateAppOrTenant(main, updatedConfig, shouldProtectProtectedConfig(req),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
new ThirdPartyConfig(
config.thirdPartyConfig.enabled, newProviders.toArray(new ThirdPartyConfig.Provider[0])),
config.passwordlessConfig,
config.totpConfig,
config.mfaConfig,
config.coreConfig);

Multitenancy.addNewOrUpdateAppOrTenant(main, updatedConfig, shouldProtectProtectedConfig(req), false, true);
Expand Down
2 changes: 2 additions & 0 deletions src/test/java/io/supertokens/test/CDIVersionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,15 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
new TotpConfig(false), new MfaConfig(null, null),
config
), false);
Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig(
new TenantIdentifier(null, "a1", "t1"),
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
new TotpConfig(false), new MfaConfig(null, null),
new JsonObject()
), false);

Expand Down
Loading
Loading