Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": " Add accountSource when caching an account #8213",
"packageName": "@azure/msal-browser",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add accountSource when caching an account #8213",
"packageName": "@azure/msal-common",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "Add accountSource when caching an account 8213",
"packageName": "@azure/msal-node",
"email": "[email protected]",
"dependentChangeType": "none"
}
17 changes: 11 additions & 6 deletions lib/msal-browser/src/cache/TokenCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
TimeUtils,
AccountEntityUtils,
buildStaticAuthorityOptions,
AccountSource,
} from "@azure/msal-common/browser";
import { buildConfiguration, Configuration } from "../config/Configuration.js";
import type { SilentRequest } from "../request/SilentRequest.js";
Expand Down Expand Up @@ -110,7 +111,8 @@ export async function loadExternalTokens(
logger,
cryptoOps,
idTokenClaims,
authority
authority,
"external"
);

const idToken = await loadIdToken(
Expand Down Expand Up @@ -180,15 +182,17 @@ async function loadAccount(
logger: Logger,
cryptoObj: ICrypto,
idTokenClaims?: TokenClaims,
authority?: Authority
authority?: Authority,
accountSource?: AccountSource
): Promise<AccountEntity> {
logger.verbose("TokenCache - loading account", correlationId);

if (request.account) {
const accountEntity =
AccountEntityUtils.createAccountEntityFromAccountInfo(
request.account
);
AccountEntityUtils.createAccountEntityFromAccountInfo({
...request.account,
accountSource: "external",
});
await storage.setAccount(
accountEntity,
correlationId,
Expand Down Expand Up @@ -226,7 +230,8 @@ async function loadAccount(
claimsTenantId,
undefined, // authCodePayload
undefined, // nativeAccountId
logger
logger,
accountSource // accountSource
);

await storage.setAccount(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,10 @@ export class NestedAppAuthController implements IController {

const accountEntity =
AccountEntityUtils.createAccountEntityFromAccountInfo(
result.account,
{
...result.account,
accountSource: "naa",
},
result.cloudGraphHostName,
result.msGraphHost
);
Expand Down
7 changes: 6 additions & 1 deletion lib/msal-browser/src/controllers/StandardController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1431,7 +1431,10 @@ export class StandardController implements IController {
// Account gets saved to browser storage regardless of native or not
const accountEntity =
AccountEntityUtils.createAccountEntityFromAccountInfo(
result.account,
{
...result.account,
accountSource: "pwb",
Copy link
Collaborator

@tnorling tnorling Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use API names or a number tag, we can't assume this is a PWB call

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest using ApiId actually

},
result.cloudGraphHostName,
result.msGraphHost
);
Expand Down Expand Up @@ -1881,6 +1884,7 @@ export class StandardController implements IController {
isNativeBroker: result.fromPlatformBroker,
accessTokenSize: result.accessToken.length,
idTokenSize: result.idToken.length,
accountSource: account.accountSource,
},
undefined,
result.account
Expand All @@ -1900,6 +1904,7 @@ export class StandardController implements IController {
atsMeasurement.end(
{
success: false,
accountSource: account.accountSource,
},
error,
account
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,9 @@ export class PlatformAuthInteractionClient extends BaseInteractionClient {
undefined, // environment
idTokenClaims.tid,
undefined, // auth code payload
response.account.id
response.account.id,
this.logger,
"platform_broker" // accountSource - response from native/platform broker (WAM)
);

// Ensure expires_in is in number format
Expand Down
58 changes: 57 additions & 1 deletion lib/msal-browser/test/app/PublicClientApplication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5350,6 +5350,60 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
pca.acquireTokenSilent(silentRequest);
});

it("emits accountSource in performance event when account has accountSource", (done) => {
const testIdTokenClaims: TokenClaims = {
ver: "2.0",
iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0",
sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ",
name: "Abe Lincoln",
preferred_username: "[email protected]",
oid: "00000000-0000-0000-66f3-3332eca7ea81",
tid: "3338040d-6c67-4c5b-b112-36a304b66dad",
nonce: "123523",
login_hint: "testLoginHint",
};

const testAccount: AccountInfo = {
homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID,
localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID,
environment: "login.windows.net",
tenantId: testIdTokenClaims.tid || "",
username: testIdTokenClaims.preferred_username || "",
loginHint: testIdTokenClaims.login_hint,
idTokenClaims: { ...testIdTokenClaims },
accountSource: "external",
};

jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue(
TEST_STATE_VALUES.TEST_STATE_SILENT
);
const silentRequest: SilentRequest = {
scopes: ["User.Read"],
account: testAccount,
correlationId: RANDOM_TEST_GUID,
};

jest.spyOn(
StandardController.prototype,
<any>"acquireTokenSilentAsync"
).mockResolvedValue({
fromCache: true,
accessToken: "abc",
idToken: "defg",
account: testAccount,
});

const callbackId = pca.addPerformanceCallback((events) => {
expect(events[0].success).toBe(true);
expect(events[0].accountSource).toBe("external");

pca.removePerformanceCallback(callbackId);
done();
});

pca.acquireTokenSilent(silentRequest);
});

it("emits expect performance event when successful in case of network request", (done) => {
const testAccount: AccountInfo = {
homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID,
Expand Down Expand Up @@ -6508,7 +6562,9 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
describe("hydrateCache tests", () => {
const testAccount: AccountInfo = {
...AccountEntityUtils.getAccountInfo(
buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS)
buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS, undefined, {
accountSource: "pwb",
})
),
idTokenClaims: ID_TOKEN_CLAIMS,
idToken: TEST_TOKENS.IDTOKEN_V2,
Expand Down
32 changes: 32 additions & 0 deletions lib/msal-browser/test/cache/TokenCache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ describe("TokenCache tests", () => {
const testAccountInfo = AccountEntityUtils.getAccountInfo(
buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS, undefined, {
environment: testEnvironment,
accountSource: "external",
})
);
const testAccountKey =
Expand Down Expand Up @@ -545,5 +546,36 @@ describe("TokenCache tests", () => {
false
);
});

it("sets accountSource to 'external' when loading external tokens", async () => {
const setAccountSpy = jest.spyOn(
BrowserCacheManager.prototype,
"setAccount"
);
const request: SilentRequest = {
scopes: TEST_CONFIG.DEFAULT_SCOPES,
authority: `${TEST_URIS.DEFAULT_INSTANCE}${TEST_CONFIG.TENANT}`,
};
const response: ExternalTokenResponse = {
id_token: testIdToken,
};
const options: LoadTokenOptions = {
clientInfo: testClientInfo,
};

const result = await loadExternalTokens(
configuration,
request,
response,
options
);

expect(result.account?.accountSource).toBe("external");
expect(setAccountSpy).toHaveBeenCalledWith(
expect.objectContaining({ accountSource: "external" }),
expect.anything(),
expect.anything()
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const TEST_ACCOUNT_INFO: AccountInfo = {
...AccountEntityUtils.getAccountInfo(testAccountEntity),
idTokenClaims: ID_TOKEN_CLAIMS,
idToken: TEST_TOKENS.IDTOKEN_V2,
accountSource: "platform_broker",
};

const TEST_ID_TOKEN: IdTokenEntity = buildIdToken(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ describe("SilentIframeClient", () => {
]),
idTokenClaims: ID_TOKEN_CLAIMS,
idToken: TEST_TOKENS.IDTOKEN_V2,
accountSource: "msal",
};
const testTokenResponse: AuthenticationResult = {
authority: TEST_CONFIG.validAuthority,
Expand Down
1 change: 1 addition & 0 deletions lib/msal-browser/test/utils/StringConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ export const TEST_ACCOUNT_INFO: AccountInfo = {
name: ID_TOKEN_CLAIMS.name,
nativeAccountId: undefined,
tenantProfiles: testTenantProfilesMap,
accountSource: "msal",
};

export function getTestAuthenticationResult(): AuthenticationResult {
Expand Down
19 changes: 16 additions & 3 deletions lib/msal-common/apiReview/msal-common.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export type AccountEntity = {
tenantProfiles?: Array<TenantProfile>;
lastUpdatedAt: string;
dataBoundary?: DataBoundary;
accountSource?: AccountSource;
};

declare namespace AccountEntityUtils {
Expand Down Expand Up @@ -196,8 +197,14 @@ export type AccountInfo = {
authorityType?: string;
tenantProfiles?: Map<string, TenantProfile>;
dataBoundary?: DataBoundary;
accountSource?: AccountSource;
};

// Warning: (ae-missing-release-tag) "AccountSource" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type AccountSource = "msal" | "external" | "pwb" | "naa" | "platform_broker";

// Warning: (ae-missing-release-tag) "ActiveAccountFilters" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down Expand Up @@ -993,7 +1000,7 @@ const BROKER_REDIRECT_URI = "brk_redirect_uri";
// Warning: (ae-missing-release-tag) "buildAccountToCache" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function buildAccountToCache(cacheStorage: CacheManager, authority: Authority, homeAccountId: string, base64Decode: (input: string) => string, correlationId: string, idTokenClaims?: TokenClaims, clientInfo?: string, environment?: string, claimsTenantId?: string | null, authCodePayload?: AuthorizationCodePayload, nativeAccountId?: string, logger?: Logger): AccountEntity;
export function buildAccountToCache(cacheStorage: CacheManager, authority: Authority, homeAccountId: string, base64Decode: (input: string) => string, correlationId: string, idTokenClaims?: TokenClaims, clientInfo?: string, environment?: string, claimsTenantId?: string | null, authCodePayload?: AuthorizationCodePayload, nativeAccountId?: string, logger?: Logger, accountSource?: AccountSource): AccountEntity;

// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (ae-incompatible-release-tags) The symbol "buildClientConfiguration" is marked as @public, but its signature references "ClientConfiguration" which is marked as @internal
Expand Down Expand Up @@ -1842,6 +1849,7 @@ function createAccountEntity(accountDetails: {
environment?: string;
nativeAccountId?: string;
tenantProfiles?: Array<TenantProfile>;
accountSource?: AccountSource;
}, authority: Authority, base64Decode?: (input: string) => string): AccountEntity;

// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
Expand Down Expand Up @@ -1985,6 +1993,11 @@ const CredentialType: {
// @public (undocumented)
type CredentialType = (typeof CredentialType)[keyof typeof CredentialType];

// Warning: (ae-missing-release-tag) "DataBoundary" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type DataBoundary = "EU" | "None";

// Warning: (ae-missing-release-tag) "DEFAULT_AUTHORITY" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down Expand Up @@ -3492,6 +3505,7 @@ export type PerformanceEvent = {
navigateCallbackResult?: boolean;
dataBoundary?: DataBoundary;
logs?: string;
accountSource?: AccountSource;
};

declare namespace PerformanceEvents {
Expand Down Expand Up @@ -4776,7 +4790,6 @@ const X_MS_LIB_CAPABILITY_VALUE: string;
// src/cache/CacheManager.ts:1840:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/cache/CacheManager.ts:1841:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/cache/CacheManager.ts:1849:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/cache/entities/AccountEntity.ts:49:5 - (ae-forgotten-export) The symbol "DataBoundary" needs to be exported by the entry point index.d.ts
// src/cache/utils/CacheTypes.ts:93:53 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
// src/cache/utils/CacheTypes.ts:93:43 - (tsdoc-malformed-html-name) Invalid HTML element: An HTML name must be an ASCII letter followed by zero or more letters, digits, or hyphens
// src/client/AuthorizationCodeClient.ts:221:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
Expand All @@ -4796,9 +4809,9 @@ const X_MS_LIB_CAPABILITY_VALUE: string;
// src/index.ts:8:12 - (tsdoc-characters-after-block-tag) The token "@azure" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@"
// src/index.ts:8:4 - (tsdoc-undefined-tag) The TSDoc tag "@module" is not defined in this configuration
// src/request/AuthenticationHeaderParser.ts:74:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/response/ResponseHandler.ts:346:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/response/ResponseHandler.ts:347:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/response/ResponseHandler.ts:348:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/response/ResponseHandler.ts:349:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/telemetry/performance/PerformanceClient.ts:673:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/telemetry/performance/PerformanceClient.ts:673:15 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// src/telemetry/performance/PerformanceClient.ts:685:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
Expand Down
17 changes: 17 additions & 0 deletions lib/msal-common/src/account/AccountInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ import { TokenClaims } from "./TokenClaims.js";

export type DataBoundary = "EU" | "None";

/**
* Indicates the source of how an account was created and cached.
* - "msal" - Account was created directly by MSAL from a network token response
* - "external" - Account was loaded from an external source via loadExternalTokens
* - "pwb" - Account was cached from a pairwise broker response (e.g., PairwiseBrokerApplication)
* - "naa" - Account was cached from a nested app auth host response (e.g., EmbeddedClientApplication)
* - "platform_broker" - Account was cached from a native/platform broker response (e.g., WAM)
*/
export type AccountSource =
| "msal"
| "external"
| "pwb"
| "naa"
| "platform_broker";

/**
* Account object with the following signature:
* - homeAccountId - Home account identifier for this account object
Expand All @@ -20,6 +35,7 @@ export type DataBoundary = "EU" | "None";
* - nativeAccountId - The user's native account ID
* - tenantProfiles - Map of tenant profile objects for each tenant that the account has authenticated with in the browser
* - dataBoundary - Data boundary extracted from clientInfo
* - accountSource - Source of how the account was created (msal, external, pwb, naa, or platform_broker)
*/
export type AccountInfo = {
homeAccountId: string;
Expand All @@ -43,6 +59,7 @@ export type AccountInfo = {
authorityType?: string;
tenantProfiles?: Map<string, TenantProfile>;
dataBoundary?: DataBoundary;
accountSource?: AccountSource;
};

/**
Expand Down
Loading
Loading