Add support for refreshing access tokens

This commit is contained in:
Laurent Nguyen 2025-03-04 14:00:18 +01:00
parent c1832477cd
commit f607b5abd3
5 changed files with 82 additions and 24 deletions

View File

@ -4,6 +4,7 @@
export enum FabricMessageTypes { export enum FabricMessageTypes {
GetAuthorizationToken = "GetAuthorizationToken", GetAuthorizationToken = "GetAuthorizationToken",
GetAllResourceTokens = "GetAllResourceTokens", GetAllResourceTokens = "GetAllResourceTokens",
GetAccessToken = "GetAccessToken",
Ready = "Ready", Ready = "Ready",
} }

View File

@ -73,6 +73,14 @@ export type FabricMessageV3 =
message: { message: {
visible: boolean; visible: boolean;
}; };
}
| {
type: "accessToken";
message: {
id: string;
error: string | undefined;
data: { accessToken: string };
};
}; };
export enum CosmosDbArtifactType { export enum CosmosDbArtifactType {

View File

@ -11,7 +11,7 @@ import { IGalleryItem } from "Juno/JunoClient";
import { import {
isFabricMirrored, isFabricMirrored,
isFabricMirroredKey, isFabricMirroredKey,
scheduleRefreshDatabaseResourceToken, scheduleRefreshFabricToken,
} from "Platform/Fabric/FabricUtil"; } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
@ -356,7 +356,7 @@ export default class Explorer {
public onRefreshResourcesClick = async (): Promise<void> => { public onRefreshResourcesClick = async (): Promise<void> => {
if (isFabricMirroredKey()) { if (isFabricMirroredKey()) {
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases()); scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
return; return;
} }

View File

@ -12,17 +12,41 @@ let timeoutId: NodeJS.Timeout | undefined;
// Prevents multiple parallel requests during DEBOUNCE_DELAY_MS // Prevents multiple parallel requests during DEBOUNCE_DELAY_MS
let lastRequestTimestamp: number | undefined = undefined; let lastRequestTimestamp: number | undefined = undefined;
const requestDatabaseResourceTokens = async (): Promise<void> => { /**
* Request fabric token:
* - Mirrored key and AAD: Database Resource Tokens
* - Native: AAD token
* @returns
*/
const requestFabricToken = async (): Promise<void> => {
if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) { if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) {
return; return;
} }
if (!userContext.fabricContext || !userContext.databaseAccount) { if (!userContext.fabricContext || !userContext.databaseAccount) {
// This should not happen
logConsoleError("Fabric context or database account is missing: cannot request tokens");
return; return;
} }
lastRequestTimestamp = Date.now(); lastRequestTimestamp = Date.now();
try { try {
if (isFabricMirrored()) {
await requestAndStoreDatabaseResourceTokens();
} else if (isFabricNative()) {
await requestAndStoreAccessToken();
}
scheduleRefreshFabricToken();
} catch (error) {
logConsoleError(error as string);
throw error;
} finally {
lastRequestTimestamp = undefined;
}
};
const requestAndStoreDatabaseResourceTokens = async (): Promise<void> => {
const resourceTokenInfo = await sendCachedDataMessage<ResourceTokenInfo>( const resourceTokenInfo = await sendCachedDataMessage<ResourceTokenInfo>(
FabricMessageTypes.GetAllResourceTokens, FabricMessageTypes.GetAllResourceTokens,
[], [],
@ -33,6 +57,21 @@ const requestDatabaseResourceTokens = async (): Promise<void> => {
userContext.databaseAccount.properties.documentEndpoint = resourceTokenInfo.endpoint; userContext.databaseAccount.properties.documentEndpoint = resourceTokenInfo.endpoint;
} }
if (resourceTokenInfo.credentialType === "OAuth2") {
// Mirrored AAD
updateUserContext({
fabricContext: {
...userContext.fabricContext,
databaseName: resourceTokenInfo.databaseId,
artifactInfo: undefined,
isReadOnly: resourceTokenInfo.isReadOnly ?? userContext.fabricContext.isReadOnly,
},
databaseAccount: { ...userContext.databaseAccount },
aadToken: resourceTokenInfo.accessToken,
});
} else {
// TODO: In Fabric contract V2, credentialType is undefined. For V3, it is "Key". Check for "Key" when V3 is supported for Fabric Mirroring Key
// Mirrored key
updateUserContext({ updateUserContext({
fabricContext: { fabricContext: {
...userContext.fabricContext, ...userContext.fabricContext,
@ -45,21 +84,27 @@ const requestDatabaseResourceTokens = async (): Promise<void> => {
}, },
databaseAccount: { ...userContext.databaseAccount }, databaseAccount: { ...userContext.databaseAccount },
}); });
scheduleRefreshDatabaseResourceToken();
} catch (error) {
logConsoleError(error as string);
throw error;
} finally {
lastRequestTimestamp = undefined;
} }
}; };
const requestAndStoreAccessToken = async (): Promise<void> => {
const accessTokenInfo = await sendCachedDataMessage<{ accessToken: string }>(
FabricMessageTypes.GetAccessToken,
[],
userContext.fabricContext.artifactInfo?.connectionId,
);
updateUserContext({
aadToken: accessTokenInfo.accessToken,
});
};
/** /**
* Check token validity and schedule a refresh if necessary * Check token validity and schedule a refresh if necessary
* @param tokenTimestamp * @param tokenTimestamp
* @returns * @returns
*/ */
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => { export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void> => {
return new Promise((resolve) => { return new Promise((resolve) => {
if (timeoutId !== undefined) { if (timeoutId !== undefined) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
@ -68,7 +113,7 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom
timeoutId = setTimeout( timeoutId = setTimeout(
() => { () => {
requestDatabaseResourceTokens().then(resolve); requestFabricToken().then(resolve);
}, },
refreshNow ? 0 : TOKEN_VALIDITY_MS, refreshNow ? 0 : TOKEN_VALIDITY_MS,
); );
@ -77,7 +122,7 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => { export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) { if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
scheduleRefreshDatabaseResourceToken(true); scheduleRefreshFabricToken(true);
} }
}; };

View File

@ -14,7 +14,7 @@ import { useDialog } from "Explorer/Controls/Dialog";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
import { import {
AppStateComponentNames, AppStateComponentNames,
OPEN_TABS_SUBCOMPONENT_NAME, OPEN_TABS_SUBCOMPONENT_NAME,
@ -154,7 +154,7 @@ async function configureFabric(): Promise<Explorer> {
}; };
explorer = createExplorerFabricLegacy(initializationMessage, data.version); explorer = createExplorerFabricLegacy(initializationMessage, data.version);
await scheduleRefreshDatabaseResourceToken(true); await scheduleRefreshFabricToken(true);
resolve(explorer); resolve(explorer);
await explorer.refreshAllDatabases(); await explorer.refreshAllDatabases();
if (userContext.fabricContext.isVisible) { if (userContext.fabricContext.isVisible) {
@ -169,9 +169,12 @@ async function configureFabric(): Promise<Explorer> {
if (initializationMessage.artifactType === CosmosDbArtifactType.MIRRORED_KEY) { if (initializationMessage.artifactType === CosmosDbArtifactType.MIRRORED_KEY) {
// Do not show Home tab for Mirrored // Do not show Home tab for Mirrored
useTabs.getState().closeReactTab(ReactTabKind.Home); useTabs.getState().closeReactTab(ReactTabKind.Home);
await scheduleRefreshDatabaseResourceToken(true);
} }
// All tokens used in fabric expire
// For Mirrored key, we need the token right away to get the database and containers list.
await scheduleRefreshFabricToken(isFabricMirroredKey());
resolve(explorer); resolve(explorer);
await explorer.refreshAllDatabases(); await explorer.refreshAllDatabases();
@ -188,7 +191,8 @@ async function configureFabric(): Promise<Explorer> {
explorer.onNewCollectionClicked(); explorer.onNewCollectionClicked();
break; break;
case "authorizationToken": case "authorizationToken":
case "allResourceTokens_v2": { case "allResourceTokens_v2":
case "accessToken": {
handleCachedDataMessage(data); handleCachedDataMessage(data);
break; break;
} }