Remove connection string validation for connection string login

- Remove validateDirectConnectionStringLogin and its helpers (extractEndpointHostFromConnectionString, extractHostToken, directLoginAllowlistedEndpointZones)
- Remove old extractMasterKeyfromConnectionString (Gremlin-specific), rename extractAccountKeyFromConnectionString to extractMasterKeyfromConnectionString
- Change validateDirectConnectionStringConnectivity to throw on error instead of returning string|undefined
- Simplify direct-login flow: submit connection string to CosmosClient as-is, no format or endpoint validation
- Keep connectivity pre-check (throws if CosmosClient cannot reach account)
- Remove 6 unused localization keys for validation errors
- Update tests to match new behavior
This commit is contained in:
Asier Isayas
2026-08-17 06:47:22 -07:00
parent a2d24cc972
commit ea42796379
7 changed files with 29 additions and 266 deletions
+1 -1
View File
@@ -159,7 +159,7 @@ describe("HostedExplorer tryCosmosDB postMessage handler", () => {
}); });
it("does not open the Data Explorer when the Cosmos client cannot connect", async () => { it("does not open the Data Explorer when the Cosmos client cannot connect", async () => {
mockValidateDirectConnectionStringConnectivity.mockResolvedValue("Unable to connect to the account."); mockValidateDirectConnectionStringConnectivity.mockRejectedValue(new Error("Unable to connect to the account."));
const { container } = render(<App />); const { container } = render(<App />);
const validConnStr = `AccountEndpoint=https://${FAKE_ACCOUNT_NAME}.documents.azure.com:443/;AccountKey=${FAKE_KEY};`; const validConnStr = `AccountEndpoint=https://${FAKE_ACCOUNT_NAME}.documents.azure.com:443/;AccountKey=${FAKE_KEY};`;
+4 -28
View File
@@ -23,12 +23,7 @@ import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
import "./Platform/Hosted/ConnectScreen.less"; import "./Platform/Hosted/ConnectScreen.less";
import { parseConnectionString } from "./Platform/Hosted/Helpers/ConnectionStringParser"; import { parseConnectionString } from "./Platform/Hosted/Helpers/ConnectionStringParser";
import { isResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils"; import { isResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils";
import { import { extractMasterKeyfromConnectionString, isDirectConnectionStringLoginApi } from "./Platform/Hosted/HostedUtils";
extractAccountKeyFromConnectionString,
extractMasterKeyfromConnectionString,
isDirectConnectionStringLoginApi,
validateDirectConnectionStringLogin,
} from "./Platform/Hosted/HostedUtils";
import "./Shared/appInsights"; import "./Shared/appInsights";
import { allowedHostedExplorerEndpoints } from "./Utils/EndpointUtils"; import { allowedHostedExplorerEndpoints } from "./Utils/EndpointUtils";
import { useAADAuth } from "./hooks/useAADAuth"; import { useAADAuth } from "./hooks/useAADAuth";
@@ -74,31 +69,14 @@ const App: React.FunctionComponent = () => {
if (metadata && isDirectConnectionStringLoginApi(metadata.apiKind)) { if (metadata && isDirectConnectionStringLoginApi(metadata.apiKind)) {
// SQL, Tables, and Gremlin sign data-plane requests client-side with the account key, so we skip // SQL, Tables, and Gremlin sign data-plane requests client-side with the account key, so we skip
// the Portal Backend proxy and use the metadata derived from the connection string directly. // the Portal Backend proxy and use the metadata derived from the connection string directly.
// Validate the host and account client-side (mirrors the backend's ValidateHostAndAccount).
const validationError = validateDirectConnectionStringLogin(connStr, metadata);
if (validationError) {
logError(
`Rejected connection string for direct login: ${validationError}`,
"HostedExplorer/connectWithConnectionString",
);
return;
}
// Only open the view once we confirm the Cosmos client can actually connect.
validateDirectConnectionStringConnectivity(connStr, metadata) validateDirectConnectionStringConnectivity(connStr, metadata)
.then((connectivityError) => { .then(() => {
if (connectivityError) {
logError(
`Rejected connection string for direct login: ${connectivityError}`,
"HostedExplorer/connectWithConnectionString",
);
return;
}
setDirectLoginMetadata(metadata); setDirectLoginMetadata(metadata);
setAuthType(AuthType.ConnectionString); setAuthType(AuthType.ConnectionString);
}) })
.catch((error) => { .catch((error) => {
logError( logError(
`Failed to validate connection string for direct login: ${error}`, `Failed to connect with connection string: ${error}`,
"HostedExplorer/connectWithConnectionString", "HostedExplorer/connectWithConnectionString",
); );
}); });
@@ -167,9 +145,7 @@ const App: React.FunctionComponent = () => {
authType: AuthType.ConnectionString, authType: AuthType.ConnectionString,
encryptedToken, encryptedToken,
encryptedTokenMetadata: accountMetadata, encryptedTokenMetadata: accountMetadata,
masterKey: directLoginMetadata masterKey: extractMasterKeyfromConnectionString(connectionString),
? extractAccountKeyFromConnectionString(connectionString)
: extractMasterKeyfromConnectionString(connectionString),
}; };
} else if (authType === AuthType.ResourceToken) { } else if (authType === AuthType.ResourceToken) {
frameWindow.hostedConfig = { frameWindow.hostedConfig = {
+2 -9
View File
@@ -1177,15 +1177,8 @@
}, },
"connectExplorer": { "connectExplorer": {
"errors": { "errors": {
"connectionStringMissing": "Connection string is missing.", "connectFailed": "Failed to connect using the provided connection string. Please verify it is correct and try again.",
"accountNameMissing": "Account name is missing from the connection string.", "connectivityUnreachable": "Unable to connect to the account. Please verify the connection string is correct and that the account is reachable."
"endpointHostMissing": "Endpoint host is missing from the connection string.",
"endpointHostNotAllowed": "Endpoint host is not allowed.",
"accountNameMismatch": "Account name does not match the endpoint host.",
"accountKeyMissing": "Account key is missing from the connection string.",
"connectivityInvalid": "Unable to connect to the account with the provided connection string.",
"connectivityUnreachable": "Unable to connect to the account. Please verify the connection string is correct and that the account is reachable.",
"connectFailed": "Failed to connect using the provided connection string. Please verify it is correct and try again."
} }
} }
} }
@@ -11,11 +11,7 @@ import { configContext } from "../../../ConfigContext";
import { AccessInputMetadata } from "../../../Contracts/DataModels"; import { AccessInputMetadata } from "../../../Contracts/DataModels";
import { parseConnectionString } from "../Helpers/ConnectionStringParser"; import { parseConnectionString } from "../Helpers/ConnectionStringParser";
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils"; import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
import { import { extractMasterKeyfromConnectionString, isDirectConnectionStringLoginApi } from "../HostedUtils";
extractAccountKeyFromConnectionString,
isDirectConnectionStringLoginApi,
validateDirectConnectionStringLogin,
} from "../HostedUtils";
interface Props { interface Props {
connectionString: string; connectionString: string;
@@ -52,21 +48,17 @@ export const isAccountRestrictedForConnectionStringLogin = async (connectionStri
return (await response.text()).toLowerCase() === "true"; return (await response.text()).toLowerCase() === "true";
}; };
// Verifies the account key can actually authenticate against the account by making a lightweight read of // Verifies the connection string can actually authenticate against the account by making a lightweight
// the database account through the Cosmos client. Returns undefined on success, or an error message when // read of the database account through the Cosmos client. Throws on failure.
// the client cannot connect.
export const validateDirectConnectionStringConnectivity = async ( export const validateDirectConnectionStringConnectivity = async (
connectionString: string, connectionString: string,
metadata: AccessInputMetadata, metadata: AccessInputMetadata,
): Promise<string | undefined> => { ): Promise<void> => {
const masterKey = extractAccountKeyFromConnectionString(connectionString); const masterKey = extractMasterKeyfromConnectionString(connectionString);
if (!metadata?.documentEndpoint || !masterKey) { if (!metadata?.documentEndpoint || !masterKey) {
return t(Keys.connectExplorer.errors.connectivityInvalid); throw new Error(t(Keys.connectExplorer.errors.connectivityUnreachable));
} }
// The Cosmos client reads its connection settings from userContext, so write the master key and
// endpoint there. Then we issue a lightweight authenticated read to confirm the
// credentials work.
updateUserContext({ updateUserContext({
authType: AuthType.ConnectionString, authType: AuthType.ConnectionString,
masterKey, masterKey,
@@ -76,9 +68,8 @@ export const validateDirectConnectionStringConnectivity = async (
try { try {
await client().getDatabaseAccount(); await client().getDatabaseAccount();
return undefined;
} catch { } catch {
return t(Keys.connectExplorer.errors.connectivityUnreachable); throw new Error(t(Keys.connectExplorer.errors.connectivityUnreachable));
} }
}; };
@@ -125,23 +116,13 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
if (metadata && isDirectConnectionStringLoginApi(metadata.apiKind)) { if (metadata && isDirectConnectionStringLoginApi(metadata.apiKind)) {
// SQL, Tables, and Gremlin sign data-plane requests client-side with the account key, so // SQL, Tables, and Gremlin sign data-plane requests client-side with the account key, so
// we skip the Portal Backend proxy and use the metadata parsed from the connection string. // we skip the Portal Backend proxy and use the metadata parsed from the connection string.
// Validate the host and account client-side (mirrors the backend's ValidateHostAndAccount). try {
const validationError = validateDirectConnectionStringLogin(connectionString, metadata); await validateDirectConnectionStringConnectivity(connectionString, metadata);
if (validationError) { setDirectLoginMetadata(metadata);
setErrorMessage(validationError); setAuthType(AuthType.ConnectionString);
return; } catch (error) {
setErrorMessage(error.message);
} }
// Only open the view once we confirm the Cosmos client can actually connect.
const connectivityError = await validateDirectConnectionStringConnectivity(
connectionString,
metadata,
);
if (connectivityError) {
setErrorMessage(connectivityError);
return;
}
setDirectLoginMetadata(metadata);
setAuthType(AuthType.ConnectionString);
return; return;
} }
@@ -150,7 +131,7 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
setEncryptedToken(encryptedToken); setEncryptedToken(encryptedToken);
setAuthType(AuthType.ConnectionString); setAuthType(AuthType.ConnectionString);
} catch (error) { } catch (error) {
setErrorMessage(t(Keys.connectExplorer.errors.connectFailed)); setErrorMessage(t(Keys.connectExplorer.errors.connectivityUnreachable));
} }
}} }}
> >
+7 -88
View File
@@ -1,10 +1,8 @@
import { AccessInputMetadata, ApiKind } from "../../Contracts/DataModels"; import { AccessInputMetadata, ApiKind } from "../../Contracts/DataModels";
import { import {
extractAccountKeyFromConnectionString, extractMasterKeyfromConnectionString,
extractEndpointHostFromConnectionString,
getDatabaseAccountPropertiesFromMetadata, getDatabaseAccountPropertiesFromMetadata,
isDirectConnectionStringLoginApi, isDirectConnectionStringLoginApi,
validateDirectConnectionStringLogin,
} from "./HostedUtils"; } from "./HostedUtils";
describe("getDatabaseAccountPropertiesFromMetadata", () => { describe("getDatabaseAccountPropertiesFromMetadata", () => {
@@ -37,13 +35,13 @@ describe("getDatabaseAccountPropertiesFromMetadata", () => {
}); });
}); });
describe("extractAccountKeyFromConnectionString", () => { describe("extractMasterKeyfromConnectionString", () => {
const mockAccountName = "Test"; const mockAccountName = "Test";
const mockKey = "abc123+/=someKey=="; const mockKey = "abc123+/=someKey==";
it("extracts the account key from a SQL connection string", () => { it("extracts the account key from a SQL connection string", () => {
expect( expect(
extractAccountKeyFromConnectionString( extractMasterKeyfromConnectionString(
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockKey};`, `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockKey};`,
), ),
).toBe(mockKey); ).toBe(mockKey);
@@ -51,7 +49,7 @@ describe("extractAccountKeyFromConnectionString", () => {
it("extracts the account key from a Table connection string", () => { it("extracts the account key from a Table connection string", () => {
expect( expect(
extractAccountKeyFromConnectionString( extractMasterKeyfromConnectionString(
`DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;`, `DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;`,
), ),
).toBe(mockKey); ).toBe(mockKey);
@@ -59,7 +57,7 @@ describe("extractAccountKeyFromConnectionString", () => {
it("extracts the account key from a Gremlin connection string", () => { it("extracts the account key from a Gremlin connection string", () => {
expect( expect(
extractAccountKeyFromConnectionString( extractMasterKeyfromConnectionString(
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockKey};ApiKind=Gremlin;`, `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockKey};ApiKind=Gremlin;`,
), ),
).toBe(mockKey); ).toBe(mockKey);
@@ -67,12 +65,12 @@ describe("extractAccountKeyFromConnectionString", () => {
it("returns undefined when there is no account key", () => { it("returns undefined when there is no account key", () => {
expect( expect(
extractAccountKeyFromConnectionString(`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;`), extractMasterKeyfromConnectionString(`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;`),
).toBeUndefined(); ).toBeUndefined();
}); });
it("returns undefined for an empty connection string", () => { it("returns undefined for an empty connection string", () => {
expect(extractAccountKeyFromConnectionString("")).toBeUndefined(); expect(extractMasterKeyfromConnectionString("")).toBeUndefined();
}); });
}); });
@@ -89,82 +87,3 @@ describe("isDirectConnectionStringLoginApi", () => {
expect(isDirectConnectionStringLoginApi(ApiKind.Cassandra)).toBe(false); expect(isDirectConnectionStringLoginApi(ApiKind.Cassandra)).toBe(false);
}); });
}); });
describe("extractEndpointHostFromConnectionString", () => {
it("extracts the host from a SQL/Gremlin AccountEndpoint", () => {
expect(
extractEndpointHostFromConnectionString(
"AccountEndpoint=https://my-account.documents.azure.com:443/;AccountKey=key==;",
),
).toBe("my-account.documents.azure.com");
});
it("extracts the host from a Table TableEndpoint", () => {
expect(
extractEndpointHostFromConnectionString(
"DefaultEndpointsProtocol=https;AccountName=my-account;AccountKey=key==;TableEndpoint=https://my-account.table.cosmos.azure.com:443/;",
),
).toBe("my-account.table.cosmos.azure.com");
});
it("returns undefined when no endpoint host is present", () => {
expect(extractEndpointHostFromConnectionString("AccountName=my-account;AccountKey=key==;")).toBeUndefined();
});
});
describe("validateDirectConnectionStringLogin", () => {
const mockKey = "abc123+/=someKey==";
const sqlMetadata = { accountName: "my-account", apiKind: ApiKind.SQL } as AccessInputMetadata;
const tableMetadata = { accountName: "my-account", apiKind: ApiKind.Table } as AccessInputMetadata;
const gremlinMetadata = { accountName: "my-account", apiKind: ApiKind.Graph } as AccessInputMetadata;
it("returns undefined for a valid SQL connection string", () => {
const connectionString = `AccountEndpoint=https://my-account.documents.azure.com:443/;AccountKey=${mockKey};`;
expect(validateDirectConnectionStringLogin(connectionString, sqlMetadata)).toBeUndefined();
});
it("returns undefined for a valid Table connection string using the cosmos.azure.com zone", () => {
const connectionString = `DefaultEndpointsProtocol=https;AccountName=my-account;AccountKey=${mockKey};TableEndpoint=https://my-account.table.cosmos.azure.com:443/;`;
expect(validateDirectConnectionStringLogin(connectionString, tableMetadata)).toBeUndefined();
});
it("returns undefined for a valid Gremlin connection string", () => {
const connectionString = `AccountEndpoint=https://my-account.documents.azure.com:443/;AccountKey=${mockKey};ApiKind=Gremlin;`;
expect(validateDirectConnectionStringLogin(connectionString, gremlinMetadata)).toBeUndefined();
});
it("rejects an empty connection string", () => {
expect(validateDirectConnectionStringLogin("", {} as AccessInputMetadata)).toBe("Connection string is missing.");
});
it("rejects when the account name is missing from metadata", () => {
const connectionString = `AccountEndpoint=https://my-account.documents.azure.com:443/;AccountKey=${mockKey};`;
expect(validateDirectConnectionStringLogin(connectionString, {} as AccessInputMetadata)).toBe(
"Account name is missing from the connection string.",
);
});
it("rejects a host that is not in an allowlisted zone", () => {
// Point the endpoint host at an untrusted zone while keeping a valid parsed account name.
const tamperedConnectionString = `AccountEndpoint=https://my-account.evil.example.com:443/;AccountKey=${mockKey};`;
expect(validateDirectConnectionStringLogin(tamperedConnectionString, sqlMetadata)).toBe(
"Endpoint host is not allowed.",
);
});
it("rejects when the account name does not match the endpoint host label", () => {
const connectionString = `AccountEndpoint=https://real-account.documents.azure.com:443/;AccountKey=${mockKey};`;
const metadata = { accountName: "different-account", apiKind: ApiKind.SQL } as AccessInputMetadata;
expect(validateDirectConnectionStringLogin(connectionString, metadata)).toBe(
"Account name does not match the endpoint host.",
);
});
it("rejects when the account key is missing", () => {
const connectionString = "AccountEndpoint=https://my-account.documents.azure.com:443/;";
const metadata = { accountName: "my-account", apiKind: ApiKind.SQL } as AccessInputMetadata;
expect(validateDirectConnectionStringLogin(connectionString, metadata)).toBe(
"Account key is missing from the connection string.",
);
});
});
+1 -106
View File
@@ -1,6 +1,5 @@
import { AccountKind, CapabilityNames } from "../../Common/Constants"; import { AccountKind, CapabilityNames } from "../../Common/Constants";
import { AccessInputMetadata, ApiKind } from "../../Contracts/DataModels"; import { AccessInputMetadata, ApiKind } from "../../Contracts/DataModels";
import { Keys, t } from "../../Localization";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -41,15 +40,9 @@ export function getDatabaseAccountKindFromExperience(apiExperience: typeof userC
return AccountKind.GlobalDocumentDB; return AccountKind.GlobalDocumentDB;
} }
export function extractMasterKeyfromConnectionString(connectionString: string): string | undefined {
// Only Gremlin uses the actual master key for connection to cosmos
const matchedParts = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$");
return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined;
}
// Extracts the account key from any Cosmos connection string. The account key value cannot contain a // Extracts the account key from any Cosmos connection string. The account key value cannot contain a
// semicolon, so we capture everything up to the next connection-string delimiter. // semicolon, so we capture everything up to the next connection-string delimiter.
export function extractAccountKeyFromConnectionString(connectionString: string): string | undefined { export function extractMasterKeyfromConnectionString(connectionString: string): string | undefined {
const matchedParts = connectionString?.match(/AccountKey=([^;]*)/); const matchedParts = connectionString?.match(/AccountKey=([^;]*)/);
return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined; return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined;
} }
@@ -59,101 +52,3 @@ export function extractAccountKeyFromConnectionString(connectionString: string):
export function isDirectConnectionStringLoginApi(apiKind: ApiKind): boolean { export function isDirectConnectionStringLoginApi(apiKind: ApiKind): boolean {
return apiKind === ApiKind.SQL || apiKind === ApiKind.Table || apiKind === ApiKind.Graph; return apiKind === ApiKind.SQL || apiKind === ApiKind.Table || apiKind === ApiKind.Graph;
} }
// DNS zones a SQL, Tables, or Gremlin connection-string endpoint host is allowed to belong to. Mirrors
// the allowlist enforced by the Portal Backend's
// ConnectionStringAccessProvider.ValidateHostAndAccount for the direct-login APIs. Both the newer
// `table.cosmos.azure.com` and legacy `table.cosmosdb.azure.com` Tables zones are accepted.
const directLoginAllowlistedEndpointZones = [
"documents.azure.com",
"table.cosmos.azure.com",
"table.cosmosdb.azure.com",
];
// Returns the substring of `value` starting at `startIndex` up to the first ':', '/', or '?'.
// Mirrors ExtractHostToken in the Portal Backend.
function extractHostToken(value: string, startIndex: number): string {
let end = value.length;
for (let i = startIndex; i < value.length; i++) {
const c = value[i];
if (c === ":" || c === "/" || c === "?") {
end = i;
break;
}
}
return value.substring(startIndex, end);
}
// Extracts the endpoint host from a connection string, mirroring ExtractEndpointHost in the Portal
// Backend. Handles AccountEndpoint/TableEndpoint (URI or bare host) and HostName.
export function extractEndpointHostFromConnectionString(connectionString: string): string | undefined {
for (const part of connectionString.split(";")) {
const trimmed = part.trim();
const equalsIndex = trimmed.indexOf("=");
if (equalsIndex < 0 || equalsIndex === trimmed.length - 1) {
continue;
}
const key = trimmed.substring(0, equalsIndex).trim().toLowerCase();
const value = trimmed.substring(equalsIndex + 1).trim();
if (key === "accountendpoint" || key === "tableendpoint") {
try {
return new URL(value).hostname;
} catch {
// Value may be a bare host without a scheme.
return extractHostToken(value, 0);
}
}
if (key === "hostname") {
return extractHostToken(value, 0);
}
}
return undefined;
}
// Client-side equivalent of the Portal Backend's ConnectionStringAccessProvider.ValidateHostAndAccount,
// scoped to the direct-login APIs (SQL, Tables, Gremlin). Ensures the connection string parses to an
// account name, exposes an endpoint host in an allowlisted DNS zone, that the account name matches the
// first DNS label of that host, and that an account key is present. Returns an error message when
// invalid, or undefined when the connection string is valid for direct login.
export function validateDirectConnectionStringLogin(
connectionString: string,
metadata: AccessInputMetadata,
): string | undefined {
if (!connectionString) {
return t(Keys.connectExplorer.errors.connectionStringMissing);
}
if (!metadata || !metadata.accountName) {
return t(Keys.connectExplorer.errors.accountNameMissing);
}
const host = extractEndpointHostFromConnectionString(connectionString);
if (!host) {
return t(Keys.connectExplorer.errors.endpointHostMissing);
}
// The host must belong to one of the allowlisted runtime endpoint zones.
const isAllowlistedHost = directLoginAllowlistedEndpointZones.some((zone) =>
host.toLowerCase().endsWith(`.${zone.toLowerCase()}`),
);
if (!isAllowlistedHost) {
return t(Keys.connectExplorer.errors.endpointHostNotAllowed);
}
// The account name must be the first DNS label of the host.
if (host.split(".")[0].toLowerCase() !== metadata.accountName.toLowerCase()) {
return t(Keys.connectExplorer.errors.accountNameMismatch);
}
// Direct login signs requests with the account key, so it must be present in the connection string.
if (!extractAccountKeyFromConnectionString(connectionString)) {
return t(Keys.connectExplorer.errors.accountKeyMissing);
}
return undefined;
}
-1
View File
@@ -478,7 +478,6 @@ function configureHostedWithConnectionString(config: ConnectionString): Explorer
}); });
} else { } else {
// Legacy encrypted-token proxy path (Mongo, Cassandra). // Legacy encrypted-token proxy path (Mongo, Cassandra).
// For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login
updateUserContext({ updateUserContext({
authType: AuthType.EncryptedToken, authType: AuthType.EncryptedToken,
accessToken: encodeURIComponent(config.encryptedToken), accessToken: encodeURIComponent(config.encryptedToken),