dotnet/aspnetcore · error · Error

Scopes not granted.

Error message

Scopes not granted.

What it means

After MSAL's acquireTokenSilent returns, the Blazor MSAL auth service verifies the response actually granted scopes and a non-empty access token. If response.scopes is empty or response.accessToken is an empty string, the consent was effectively empty and the token is unusable, so it throws 'Scopes not granted.' This catches the case where MSAL returns a token response that did not actually confer any of the requested scopes.

Solutions

  1. Verify the requested scopes are registered on the API's Expose an API blade in Azure/Entra and that the SPA client has those permissions granted.
  2. Force interactive consent (acquireTokenPopup/Redirect with prompt='consent') to re-prompt the user for the missing scopes.
  3. Clear the MSAL token cache and re-authenticate to obtain a fresh token with the granted scopes.
  4. Confirm the scopes string passed matches exactly the exposed scope URIs (e.g. api://guid/Scope.Name).

Example fix

// before
const response = await msal.acquireTokenSilent(req);
// after: fall back to interactive when scopes are missing
let response;
try {
  response = await msal.acquireTokenSilent(req);
  if (!response.scopes.length || !response.accessToken) {
    response = await msal.acquireTokenPopup(req);
  }
} catch {
  response = await msal.acquireTokenPopup(req);
}
Defensive patterns

Strategy: retry

Validate before calling

// After silent acquisition, validate scopes/token before use
function tokenResponseUsable(r: any): boolean {
  return Array.isArray(r?.scopes) && r.scopes.length > 0 && typeof r?.accessToken === 'string' && r.accessToken.length > 0;
}
const r = await msal.acquireTokenSilent(req);
if (!tokenResponseUsable(r)) {
  // escalate to interactive consent
  return await msal.acquireTokenPopup({ ...req, prompt: 'consent' });
}

Type guard

function hasGrantedScopes(r: unknown): boolean {
  return !!r && Array.isArray((r as any).scopes) && (r as any).scopes.length > 0 && typeof (r as any).accessToken === 'string' && (r as any).accessToken.length > 0;
}

Try / catch

try {
  return await authService.getTokenCore(scopes);
} catch (e) {
  if (/Scopes not granted/i.test((e as Error).message)) {
    // interactive consent retry
    return await msal.acquireTokenPopup({ ...silentRequest, prompt: 'consent' });
  }
  throw e;
}

Prevention

When it happens

Trigger: Thrown at line 206 when response.scopes.length === 0 || response.accessToken === '' after a successful acquireTokenSilent call. Happens when the token endpoint returns no scopes or an empty token despite a 200.

Common situations: The app requested scopes the user/admin did not consent to; tenant misconfiguration where the API scopes are not exposed/registered; incremental consent returning an empty scope set; the application ID URI mismatch between client and API registration; a stale token cache returning a degenerate response.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/0bfcc1f46c027c9f. Report an issue: GitHub.

Appendix: source

Thrown at src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts:206

    async getTokenCore(scopes?: string[]): Promise<AccessToken | undefined> {
        const account = this.getAccount();
        if (!account) {
            throw new Error('Failed to retrieve token, no account found.');
        }

        const silentRequest = {
            redirectUri: this._settings.auth?.redirectUri,
            account: account,
            scopes: scopes || this._settings.defaultAccessTokenScopes
        };

        this.debug(`Provisioning a token silently for scopes '${silentRequest.scopes}'`)
        this.trace('_msalApplication.acquireTokenSilent', silentRequest);
        const response = await this._msalApplication.acquireTokenSilent(silentRequest);
        this.trace('_msalApplication.acquireTokenSilent-response', response);

        if (response.scopes.length === 0 || response.accessToken === '') {
            throw new Error('Scopes not granted.');
        }

        const result = {
            value: response.accessToken,
            grantedScopes: response.scopes,
            expires: response.expiresOn
        };

        this.trace('getAccessToken-result', result);

        return result;
    }

    async signIn(context: AuthenticationContext) {
        this.trace('signIn', context);
        try {
            // Before we start any sign-in flow, clear out any previous state so that it doesn't pile up.
            this.purgeState();

View on GitHub (pinned to 3600ca084e)