Skip to content
Open
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
7 changes: 7 additions & 0 deletions change/@azure-msal-browser-add-tenantid-accountfilter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add optional tenantId parameter to request type to allow filtering accounts using tenantId for multi-tenant scenarios [#8601](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8601)",
"packageName": "@azure/msal-browser",
"email": "lalimasharda@microsoft.com",
"dependentChangeType": "patch"
}
6 changes: 5 additions & 1 deletion lib/msal-browser/apiReview/msal-browser.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,7 @@ export type PopupRequest = Partial<Omit<CommonAuthorizationUrlRequest, "response
popupWindowAttributes?: PopupWindowAttributes;
popupWindowParent?: Window;
overrideInteractionInProgress?: boolean;
tenantId?: string;
};
Comment thread
lalimasharda marked this conversation as resolved.

// Warning: (ae-missing-release-tag) "PopupSize" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -1335,6 +1336,7 @@ function redirectPreflightCheck(initialized: boolean, config: BrowserConfigurati
export type RedirectRequest = Partial<Omit<CommonAuthorizationUrlRequest, "responseMode" | "scopes" | "earJwk" | "codeChallenge" | "codeChallengeMethod" | "platformBroker">> & {
scopes: Array<string>;
redirectStartPage?: string;
tenantId?: string;
};

// Warning: (ae-missing-release-tag) "replaceHash" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -1447,7 +1449,9 @@ const SsoSilent = "ssoSilent";
// Warning: (ae-missing-release-tag) "SsoSilentRequest" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type SsoSilentRequest = Partial<Omit<CommonAuthorizationUrlRequest, "responseMode" | "earJwk" | "codeChallenge" | "codeChallengeMethod" | "platformBroker">>;
export type SsoSilentRequest = Partial<Omit<CommonAuthorizationUrlRequest, "responseMode" | "earJwk" | "codeChallenge" | "codeChallengeMethod" | "platformBroker">> & {
tenantId?: string;
};

// Warning: (ae-missing-release-tag) "stateInteractionTypeMismatch" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
Expand Down
46 changes: 46 additions & 0 deletions lib/msal-browser/docs/login-user.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,52 @@ try {
}
```

## Multi-Tenant Login with `tenantId`

When your application supports multiple tenants and users may be logged into more than one tenant, you can pass the optional `tenantId` parameter on the login request to ensure the correct cached account is used during platform broker flows.

This is particularly important in **platform broker flows** when a user who previously logged into Tenant A now wants to log into Tenant B using the same `loginHint`. Without `tenantId`, the cached account lookup may return the wrong tenant's account ID because `loginHint` alone cannot distinguish between tenants.


The `tenantId` value should be the **tenant GUID** (not a domain name) of the target tenant the user is signing into.

- Popup

```javascript
const loginRequest = {
scopes: ["user.read"],
loginHint: "user@contoso.com",
tenantId: "00000000-0000-0000-0000-000000000001", // Target tenant GUID
authority: "https://login.microsoftonline.com/00000000-0000-0000-0000-000000000001",
};

try {
const loginResponse = await msalInstance.loginPopup(loginRequest);
} catch (err) {
// handle error
}
```

- Redirect

```javascript
const loginRequest = {
scopes: ["user.read"],
loginHint: "user@contoso.com",
tenantId: "00000000-0000-0000-0000-000000000001", // Target tenant GUID
authority: "https://login.microsoftonline.com/00000000-0000-0000-0000-000000000001",
};

try {
msalInstance.loginRedirect(loginRequest);
} catch (err) {
// handle error
}
```

> [!NOTE]
> If you omit `tenantId`, the login behavior is unchanged from previous versions. The `tenantId` parameter is only used to filter cached accounts during platform broker flows and does not affect the authority or token endpoint used for the request.

## Account APIs

When a login call has succeeded, you can use the `getAllAccounts()` function to retrieve information about currently signed in users.
Expand Down
1 change: 1 addition & 0 deletions lib/msal-browser/src/controllers/StandardController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1889,6 +1889,7 @@ export class StandardController implements IController {
this.getAccount({
loginHint: request.loginHint,
sid: request.sid,
tenantId: request.tenantId,
}) ||
(!request.loginHint && !request.sid
? this.getActiveAccount()
Expand Down
6 changes: 6 additions & 0 deletions lib/msal-browser/src/request/PopupRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ export type PopupRequest = Partial<
* Optional flag to allow overriding an existing interaction_in_progress state for popup flows. **WARNING**: Use with caution! For usage details and examples, see the [login-user.md](../../../docs/login-user.md#handling-interaction_in_progress-errors) documentation.
*/
overrideInteractionInProgress?: boolean;
/**
* Optional tenant ID (GUID) used to filter cached accounts in multi-tenant scenarios.
* When provided, account lookup will also match on tenantId, preventing incorrect account matches
* when the same user has accounts across multiple tenants with the same loginHint.
Comment thread
lalimasharda marked this conversation as resolved.
*/
tenantId?: string;
};
6 changes: 6 additions & 0 deletions lib/msal-browser/src/request/RedirectRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ export type RedirectRequest = Partial<
* The page that should be returned to after loginRedirect or acquireTokenRedirect. This should only be used if this is different from the redirectUri and will default to the page that initiates the request. When the navigateToLoginRequestUrl config option is set to false this parameter will be ignored.
*/
redirectStartPage?: string;
/**
* Optional tenant ID (GUID) used to filter cached accounts in multi-tenant scenarios.
* When provided, account lookup will also match on tenantId, preventing incorrect account matches
* when the same user has accounts across multiple tenants with the same loginHint.
*/
tenantId?: string;
};
9 changes: 8 additions & 1 deletion lib/msal-browser/src/request/SsoSilentRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ export type SsoSilentRequest = Partial<
| "codeChallengeMethod"
| "platformBroker"
>
>;
> & {
/**
* Optional tenant ID (GUID) used to filter cached accounts in multi-tenant scenarios.
* When provided, account lookup will also match on tenantId, preventing incorrect account matches
* when the same user has accounts across multiple tenants with the same loginHint.
*/
tenantId?: string;
};
60 changes: 60 additions & 0 deletions lib/msal-browser/test/app/PublicClientApplication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7911,6 +7911,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
{
loginHint: searchLoginHintB,
sid: undefined,
tenantId: undefined,
},
"019d2855-0ed2-7b33-8f4b-ad00a1a5f4be"
);
Expand Down Expand Up @@ -7964,6 +7965,65 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
getAccountSpy.mockRestore();
getActiveAccountSpy.mockRestore();
});

it("should pass tenantId to account filter when provided on the request", async () => {
const controller = (pca as any).controller;
const cacheManager = controller.browserStorage;
// @ts-ignore
await cacheManager.setAccount(testAccount1);

const targetTenantId = "11111111-1111-1111-1111-111111111111";

jest.spyOn(controller, "getRequestCorrelationId").mockReturnValue(
"019d2855-0ed2-7b33-8f4b-ad00a1a5f4be"
);
jest.spyOn(cacheManager, "getAccountKeys").mockReturnValue([
"account-a-key",
]);

jest.spyOn(cacheManager, "getTokenKeys").mockReturnValue({
idToken: [],
accessToken: [],
refreshToken: [],
appMetadata: [],
});

const getAccountSpy = jest
.spyOn(cacheManager, "getAccount")
.mockReturnValue(testAccount1);

const getActiveAccountSpy = jest
.spyOn(controller, "getActiveAccount")
.mockReturnValue(null);

const getAccountsFilteredBySpy = jest.spyOn(
CacheManager.prototype,
"getAccountsFilteredBy"
);

const testRequest = {
scopes: ["user.read"],
loginHint: "user@contoso.com",
tenantId: targetTenantId,
};

const nativeAccountId = controller.getNativeAccountId(testRequest);

// Account in cache is for a different tenant, should not match
expect(nativeAccountId).toBe("");
expect(getAccountsFilteredBySpy).toHaveBeenCalledWith(
{
loginHint: "user@contoso.com",
sid: undefined,
tenantId: targetTenantId,
},
"019d2855-0ed2-7b33-8f4b-ad00a1a5f4be"
);

// Cleanup
getAccountSpy.mockRestore();
getActiveAccountSpy.mockRestore();
});
});

describe("activeAccount API tests", () => {
Expand Down
Loading