Skip to content

Commit 81f41cb

Browse files
CopilottnorlingCopilot
authored
Fix cache not used for getting token if scopes are empty (#7995)
The issue occurs when `acquireTokenSilent` is called with empty scopes (`scopes: []`). Instead of using cached tokens, the library throws a `ClientConfigurationError` during cache lookup and makes unnecessary API requests to Azure AD. ## Root Cause The problem is in `ScopeSet.createSearchScopes()` which is called from `CacheManager.getAccessToken()` during cache lookup. When empty scopes are passed, the `ScopeSet` constructor throws an error because it doesn't allow empty scope arrays, preventing cache lookup from completing. ## Solution Modified `ScopeSet.createSearchScopes()` to handle empty, null, or undefined scopes by providing default OIDC scopes (`openid`, `profile`, `offline_access`) before calling the constructor. This approach: - Follows the same pattern as `RequestParameterBuilder.addScopes()` which already handles empty scopes - Allows cache lookup to proceed with reasonable default scopes when no specific scopes are requested - Maintains all existing behavior for non-empty scopes - Eliminates unnecessary network requests when tokens are already cached ## Example ```javascript const { instance, accounts } = useMsal(); const account = useAccount(accounts[0]); // This now works and uses cache instead of making API calls const response = await instance.acquireTokenSilent({ scopes: [], // Empty scopes now supported account }); ``` The fix enables `acquireTokenSilent` to properly utilize cached tokens when called with empty scopes, improving performance and user experience. Fixes #6969. <!-- START COPILOT CODING AGENT TIPS --> --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to start the survey. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tnorling <5307810+tnorling@users.noreply.github.com> Co-authored-by: Thomas Norling <thomas.norling@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7fe97cf commit 81f41cb

File tree

3 files changed

+73
-2
lines changed

3 files changed

+73
-2
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Fix cache not used for getting token if scopes are empty (PR #7121)",
4+
"packageName": "@azure/msal-common",
5+
"email": "198982749+Copilot@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-common/src/request/ScopeSet.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
ClientAuthErrorCodes,
1313
createClientAuthError,
1414
} from "../error/ClientAuthError.js";
15-
import { Constants, OIDC_SCOPES } from "../utils/Constants.js";
15+
import {
16+
Constants,
17+
OIDC_SCOPES,
18+
OIDC_DEFAULT_SCOPES,
19+
} from "../utils/Constants.js";
1620

1721
/**
1822
* The ScopeSet class creates a set of scopes. Scopes are case-insensitive, unique values, so the Set object in JS makes
@@ -61,7 +65,13 @@ export class ScopeSet {
6165
* @returns
6266
*/
6367
static createSearchScopes(inputScopeString: Array<string>): ScopeSet {
64-
const scopeSet = new ScopeSet(inputScopeString);
68+
// Handle empty scopes by using default OIDC scopes for cache lookup
69+
const scopesToUse =
70+
inputScopeString && inputScopeString.length > 0
71+
? inputScopeString
72+
: [...OIDC_DEFAULT_SCOPES];
73+
74+
const scopeSet = new ScopeSet(scopesToUse);
6575
if (!scopeSet.containsOnlyOIDCScopes()) {
6676
scopeSet.removeOIDCScopes();
6777
} else {

lib/msal-common/test/request/ScopeSet.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,60 @@ describe("ScopeSet.ts", () => {
420420
});
421421
});
422422

423+
describe("createSearchScopes static method", () => {
424+
it("handles empty scopes array by using default OIDC scopes", () => {
425+
const result = ScopeSet.createSearchScopes([]);
426+
// After processing, it should have OIDC scopes except offline_access (which gets removed by the removeScope logic)
427+
const resultScopes = result.asArray();
428+
expect(resultScopes).toContain(Constants.OPENID_SCOPE);
429+
expect(resultScopes).toContain(Constants.PROFILE_SCOPE);
430+
// offline_access should be removed per the logic in createSearchScopes
431+
expect(resultScopes).not.toContain(Constants.OFFLINE_ACCESS_SCOPE);
432+
});
433+
434+
it("handles null or undefined scopes array by using default OIDC scopes", () => {
435+
// Test null input
436+
// @ts-ignore - intentionally testing null input
437+
const resultNull = ScopeSet.createSearchScopes(null);
438+
const nullScopes = resultNull.asArray();
439+
expect(nullScopes).toContain(Constants.OPENID_SCOPE);
440+
expect(nullScopes).toContain(Constants.PROFILE_SCOPE);
441+
442+
// Test undefined input
443+
// @ts-ignore - intentionally testing undefined input
444+
const resultUndefined = ScopeSet.createSearchScopes(undefined);
445+
const undefinedScopes = resultUndefined.asArray();
446+
expect(undefinedScopes).toContain(Constants.OPENID_SCOPE);
447+
expect(undefinedScopes).toContain(Constants.PROFILE_SCOPE);
448+
});
449+
450+
it("creates ScopeSet with provided scopes and processes them correctly", () => {
451+
const scopes = ["testscope1", "testscope2"];
452+
const result = ScopeSet.createSearchScopes(scopes);
453+
const resultScopes = result.asArray();
454+
expect(resultScopes).toContain("testscope1");
455+
expect(resultScopes).toContain("testscope2");
456+
// When non-OIDC scopes are present, OIDC scopes should be removed
457+
expect(resultScopes).not.toContain(Constants.OPENID_SCOPE);
458+
expect(resultScopes).not.toContain(Constants.PROFILE_SCOPE);
459+
expect(resultScopes).not.toContain(Constants.OFFLINE_ACCESS_SCOPE);
460+
});
461+
462+
it("handles scopes with only OIDC scopes by removing offline_access", () => {
463+
const oidcScopes = [
464+
Constants.OPENID_SCOPE,
465+
Constants.PROFILE_SCOPE,
466+
Constants.OFFLINE_ACCESS_SCOPE,
467+
];
468+
const result = ScopeSet.createSearchScopes(oidcScopes);
469+
const resultScopes = result.asArray();
470+
expect(resultScopes).toContain(Constants.OPENID_SCOPE);
471+
expect(resultScopes).toContain(Constants.PROFILE_SCOPE);
472+
// offline_access should be removed when only OIDC scopes are present
473+
expect(resultScopes).not.toContain(Constants.OFFLINE_ACCESS_SCOPE);
474+
});
475+
});
476+
423477
describe("Getters and Setters", () => {
424478
let requiredScopeSet: ScopeSet;
425479
let nonRequiredScopeSet: ScopeSet;

0 commit comments

Comments
 (0)