Authentication, Permissions & Errors
Exactly how tokens are accepted, which StratoLens permission each route needs, how your data scope applies, and what every error response means. Start at Troubleshooting if you arrived here from an error message.
On This Page
Token Requirements
Every request carries an Authorization: Bearer <token> header. The token must be:
- an Entra ID v2 access token,
- with audience = the StratoLens client ID (
api://<clientId>), - with scope (
scp) containingexternal_readand notaccess_as_user, - issued to a client app (
azp/appid) on the Trusted clients list, - for a user, not app-only.
Azure CLI --resource "api://<clientId>" and PowerShell -ResourceUrl "api://<clientId>" request the app's default scope. The token then carries only what the client is pre-authorized for, which is external_read.
Both scopes means a browser token
A token carrying both access_as_user and external_read is treated as a browser token and rejected with 401. Custom apps must request api://<clientId>/external_read only.
What Happens to Every Request
Checks run in this order. The first one that fails decides the response.
- The token is validated. Any problem gives
401. - Is the API on? If not:
403External API access is disabled by your StratoLens administrator. - Is the client trusted? If not:
403Client <id> is not a trusted client for this StratoLens installation. - Is it a
GETto one of the 73 listed routes? If not:403This route is not available through the external API. - If Restrict API access to granted users is on, does the user have the flag? If not:
403Your StratoLens administrator has not granted you external API access. - Does the user have the route's permission? If not:
403Access denied - The route runs and validates its own parameters (
400/404).
Permissions per Route Area
Permission names are as shown in the StratoLens UI. Each route's reference block repeats its permission.
| Permission | Routes |
|---|---|
| none (any signed-in user) | whoami |
| View Scans | getAllScans, getScanStatus, getScanWarnings |
| View Resources | Inventory, entity, role assignment, scan diff, security findings, sizing, orphaned, retirement, quota, access optimization and performance metrics routes, and the public-exposure, privileged-access, access-drift, security-config-drift, infrastructure-change and orphan-waste reports |
| View Costs | Cost explorer, anomalies and commitment routes, and the governance-audit, forecast, trend-variance and spend-accountability reports |
| View Defender Assessments | Every defender-* and getDefender* route, and defender-plan-costs |
| View Policy Data | Every policy-* route |
| View Advisor Recommendations | Every getAdvisor* route |
| View Resource Changes | getResourceGraphChanges, resource-changes-summary |
| View Azure Activity Logs | getActivityLogsInTimeWindow, activity-log-summary, activity-log-details |
| View Audit Logs | getAuditLogs |
| decided per request | searchResources (by destination), realized-savings-recommendations (by types), getChangeAttribution (either View Resource Changes or View Azure Activity Logs) |
Built-in Roles
The read permissions each built-in role holds. Custom roles may differ; the per-route permission is what counts.
| Permission | Viewer | Operator | Manager | Administrator | Cost Analyst | Resource Viewer |
|---|---|---|---|---|---|---|
| View Scans | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Resources | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Costs | ✓ | ✓ | ✓ | ✓ | ✓ | – |
| View Defender Assessments | ✓ | ✓ | ✓ | ✓ | – | – |
| View Policy Data | ✓ | ✓ | ✓ | ✓ | – | – |
| View Advisor Recommendations | ✓ | ✓ | ✓ | ✓ | ✓ | – |
| View Resource Changes | ✓ | ✓ | ✓ | ✓ | – | – |
| View Azure Activity Logs | ✓ | ✓ | ✓ | ✓ | – | – |
| View Audit Logs | – | – | ✓ | ✓ | – | – |
Data Scope
Every route returns only data inside your role-based scope: the subscriptions and management groups an administrator granted you. A user with full access sees everything. A personal view filter chosen in the browser is ignored for API calls, so a script's output does not change when you change your UI filter.
These are not narrowed by scope:
getAllScansandgetScanWarningsgetAuditLogsdefender-cve-detail(a shared CVE catalog)governance-auditauditIntegrity.auditEntryCountandauditByType(returned only to callers with View Audit Logs, otherwisenull)- Some installation-wide summary counts, noted per route
Out of scope usually means empty, not 403
When a request names a subscription outside your scope, most detail routes return an empty 200 rather than a 403. The reference pages note the exceptions.
Error Format
Every error body is JSON with a single error string:
{ "error": "<message>" }| Status | Meaning |
|---|---|
400 | A query parameter is missing or invalid. The message names the parameter. |
401 | Always exactly Authentication failed, for any token problem: missing header, expired, wrong audience, wrong scope, app-only, both scopes. Details are only in server logs. |
403 | One of the admin-controlled messages listed under What Happens to Every Request, or Access denied (missing permission), or a route's own permission message. |
404 | An unknown scan in a two-scan comparison, or an unknown entity on a few routes (noted per route). |
500 | Unexpected failure. Also returned for an unknown scanId on most single-scan routes (see Conventions, Scan IDs). |
401 never says why
Check the token-requirement list first, in this order: user sign-in (not app-only), external_read only, a trusted client, not expired.
Troubleshooting
Find your symptom, then the one-line fix.
The token request fails with a consent error (for example AADSTS65001)
Answer
The client isn't pre-authorized for external_read. An admin completes wizard step 2, then clicks Verify and add.
401 Authentication failed after az login --service-principal or --identity
Answer
That is an app-only token. Sign in as a user. Unattended automation is not supported.
401 from a custom app
Answer
The app requested both access_as_user and external_read, or the token expired. Request external_read only and get a fresh token.
401 on every call, and administrators see the migration banner
Answer
The installation hasn't completed the one-time access-token migration. An admin clicks the banner, runs the Cloud Shell script in the Enable Scoped Access Tokens drawer, clicks Verify access, then signs out and in.
401 with az rest against an http:// address
Answer
az rest sends no token over plain http. Use the token-only approach.
403 "External API access is disabled by your StratoLens administrator."
Answer
The master switch is off. An admin turns on Allow external API access and saves.
403 "Client <id> is not a trusted client for this StratoLens installation."
Answer
The client isn't on the Trusted clients list. An admin adds it with Add client.
403 "This route is not available through the external API."
Answer
The request isn't a GET, or the route isn't one of the 73 documented here. Use a listed route.
403 "Your StratoLens administrator has not granted you external API access."
Answer
Restrict API access to granted users is on and none of your grants carries the flag. An admin enables External API access on your grant or on one of your groups' grants.
403 "Access denied"
Answer
Your role lacks the route's permission. Ask for a role that includes it (see Permissions per Route Area).
403 "You do not have permission to search <destination>." or "...realized savings recommendations."
Answer
These two routes check permission per request. Narrow destination or types to what your role covers, or get the missing permission.
A browser app fails with a CORS error
Answer
The API has no CORS support. Call it from a backend or a script.
400 "time window must not exceed 366 days"
Answer
Split the period into windows of up to 366 days. See Time windows for how to chain them.
400 "<name> must list at most 200 values"
Answer
Split the list across several calls.
404 "Base scan with ID '…' not found."
Answer
That scan ID isn't on this installation. Take IDs from getAllScans.
400 "Base scan '…' must be older than compare scan '…'."
Answer
The pair is reversed or equal. Pass the older scan as baseScanId.
500 on a single-scan route with a plausible scanId
Answer
The scan doesn't exist. Most single-scan routes return 500 rather than 404 for an unknown scan. Take IDs from getAllScans.