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
- 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.
- Force interactive consent (acquireTokenPopup/Redirect with prompt='consent') to re-prompt the user for the missing scopes.
- Clear the MSAL token cache and re-authenticate to obtain a fresh token with the granted scopes.
- 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
- Register API scopes and grant the SPA client permissions in Azure/Entra.
- Fall back to interactive consent (prompt='consent') on empty scope responses.
- Clear the MSAL cache and re-authenticate when responses are degenerate.
- Confirm the requested scope URIs exactly match the exposed scopes.
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
- Failed to retrieve token, no account found.
- Could not load settings from
- Authorization requires a cascading parameter of type Task
- Cannot refresh authentication before the connection is…
- The authorization data specifies an authentication scheme…
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)