{"record":{"id":"0bfcc1f46c027c9f","repo":"dotnet/aspnetcore","slug":"scopes-not-granted","errorCode":null,"errorMessage":"Scopes not granted.","messagePattern":"Scopes not granted\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts","lineNumber":206,"sourceCode":"    async getTokenCore(scopes?: string[]): Promise<AccessToken | undefined> {\n        const account = this.getAccount();\n        if (!account) {\n            throw new Error('Failed to retrieve token, no account found.');\n        }\n\n        const silentRequest = {\n            redirectUri: this._settings.auth?.redirectUri,\n            account: account,\n            scopes: scopes || this._settings.defaultAccessTokenScopes\n        };\n\n        this.debug(`Provisioning a token silently for scopes '${silentRequest.scopes}'`)\n        this.trace('_msalApplication.acquireTokenSilent', silentRequest);\n        const response = await this._msalApplication.acquireTokenSilent(silentRequest);\n        this.trace('_msalApplication.acquireTokenSilent-response', response);\n\n        if (response.scopes.length === 0 || response.accessToken === '') {\n            throw new Error('Scopes not granted.');\n        }\n\n        const result = {\n            value: response.accessToken,\n            grantedScopes: response.scopes,\n            expires: response.expiresOn\n        };\n\n        this.trace('getAccessToken-result', result);\n\n        return result;\n    }\n\n    async signIn(context: AuthenticationContext) {\n        this.trace('signIn', context);\n        try {\n            // Before we start any sign-in flow, clear out any previous state so that it doesn't pile up.\n            this.purgeState();","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/dotnet/aspnetcore/blob/3600ca084e9c8b5f4174fc5e747f4c52d2100806/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts#L188-L224","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nconst response = await msal.acquireTokenSilent(req);\n// after: fall back to interactive when scopes are missing\nlet response;\ntry {\n  response = await msal.acquireTokenSilent(req);\n  if (!response.scopes.length || !response.accessToken) {\n    response = await msal.acquireTokenPopup(req);\n  }\n} catch {\n  response = await msal.acquireTokenPopup(req);\n}","handlingStrategy":"retry","validationCode":"// After silent acquisition, validate scopes/token before use\nfunction tokenResponseUsable(r: any): boolean {\n  return Array.isArray(r?.scopes) && r.scopes.length > 0 && typeof r?.accessToken === 'string' && r.accessToken.length > 0;\n}\nconst r = await msal.acquireTokenSilent(req);\nif (!tokenResponseUsable(r)) {\n  // escalate to interactive consent\n  return await msal.acquireTokenPopup({ ...req, prompt: 'consent' });\n}","typeGuard":"function hasGrantedScopes(r: unknown): boolean {\n  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;\n}","tryCatchPattern":"try {\n  return await authService.getTokenCore(scopes);\n} catch (e) {\n  if (/Scopes not granted/i.test((e as Error).message)) {\n    // interactive consent retry\n    return await msal.acquireTokenPopup({ ...silentRequest, prompt: 'consent' });\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["blazor","wasm","authentication","msal","token","consent","scopes"],"backgroundTag":null,"analyzedSha":"3600ca084e9c8b5f4174fc5e747f4c52d2100806","analyzedAt":"2026-08-11T16:32:30.678Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}