Forge apps cannot authenticate to the Assets REST API — CMDB scopes return 401 even after forge install --upgrade

There is no working credential-free path from a Forge app to the Atlassian Assets REST API. The Assets API (api.atlassian.com/jsm/assets/workspace/``...) is unreachable via requestJira(), and declaring CMDB OAuth scopes in manifest.yml results in 401 Unauthorized on every call — including in scheduled trigger context where there is no user session. The only working approach is direct fetch() to api.atlassian.com with Basic auth (email + API token), which is not viable for a Forge marketplace app.


Environment

  • Forge runtime: nodejs24.x

  • App type: Custom field type, admin page, scheduled trigger, workflow post-function

  • Atlassian cloud site: JSM with Assets (formerly Insight)

  • Forge CLI: latest as of May 2025


What we tried and the exact outcome

Attempt 1: requestJira() with gateway path

import { requestJira, assumeTrustedRoute } from '@forge/api';
const resp = await requestJira(
  assumeTrustedRoute(`/gateway/api/jsm/assets/workspace/${workspaceId}/v1/objectschema/list`)
);

Result: HTTP 404 — HTML Jira error page (“Oops, you’ve found a dead link”). The gateway path does not exist.


Attempt 2: requestJira() with correct relative path + CMDB scopes

Based on the Atlassian scopes documentation at

https://developer.atlassian.com/cloud/assets/assets-rest-api-guide/scopes-for-oauth-2-3LO-and-forge-apps/

we added these scopes to manifest.yml:

permissions:
  scopes:
    - read:cmdb-schema:jira
    - read:cmdb-object:jira
    - read:cmdb-attribute:jira

And called:

const resp = await requestJira(
  assumeTrustedRoute(`/jsm/assets/workspace/${workspaceId}/v1/objectschema/list`)
);

After forge deploy + forge install --upgrade:

Result: HTTP 401 {"code":401,"message":"Unauthorized"} — on every Assets API call, across all invocation contexts:

  • Resolver functions (user context)

  • Scheduled trigger functions (app context, no user session)

  • Web trigger functions (app context, no user session)

Additionally, adding the CMDB scopes broke existing working endpoints — the servicedeskapi workspace lookup (/rest/servicedeskapi/assets/workspace) which previously returned 200 also started returning 401 after the scope upgrade. This indicates the scope upgrade created a pending-consent state that invalidated the entire app token.


Attempt 3: Old Insight REST API v1

requestJira(route`/rest/insight/1.0/objectschema/list`)

Result: HTTP 404 {"detail":"No endpoint GET /rest/insight/1.0/objectschema/list."} — confirmed removed from Atlassian Cloud.


What does work

Direct fetch() to api.atlassian.com with Basic auth (email + API token), with api.atlassian.com added to the manifest egress allowlist:

const resp = await fetch(
  `https://api.atlassian.com/jsm/assets/workspace/${workspaceId}/v1/objectschema/list`,
  { headers: { Authorization: `Basic ${btoa(`${email}:${token}`)}` } }
);

This works but requires per-installation credentials, which is not viable for a marketplace app.


Root cause (our analysis)

The documentation page title is “Scopes for OAuth 2.0 (3LO) and Forge apps” but the CMDB scopes behave as 3LO-only in practice. They appear to require an interactive per-user OAuth consent flow. Forge’s installation-level scope grant (via forge install --upgrade) does not satisfy this requirement. Since Forge has no mechanism to initiate a 3LO consent flow from inside a Forge app, these scopes are effectively unusable from Forge today.


Impact

Any Forge app that needs to read or write Assets data (object schemas, object types, attributes, AQL queries) has no supported credential-free path. This blocks building a proper Forge marketplace app for Assets use cases without requiring customers to supply and store a service account email + API token — creating credential management burden and a poor installation experience.


What we’re asking for

One or more of the following:

  1. Make requestJira() route to the Assets API — add a gateway proxy so that requestJira(route\/jsm/assets/workspace/${id}/v1/…`)works from Forge, the same wayrequestJira(route`/rest/servicedeskapi/assets/workspace`)` works today for the workspace lookup.

  2. Make CMDB scopes work at the installation level — ensure that read:cmdb-schema:jira, read:cmdb-object:jira, read:cmdb-attribute:jira declared in manifest.yml are granted to the app’s installation token (not just as user-level 3LO scopes), so that requestJira() or api.asApp().requestJira() can call the Assets API without per-user consent.

  3. Document the actual state clearly — if there is a workaround or if the feature is on a roadmap, document it. The current docs page implies these scopes work for Forge apps, but they do not in practice.


References

  • Assets scopes docs: https://developer.atlassian.com/cloud/assets/assets-rest-api-guide/scopes-for-oauth-2-3LO-and-forge-apps/

  • Forge requestJira docs: https://developer.atlassian.com/platform/forge/runtime-reference/product-fetch-api/

  • Community thread: “How to Access JSM Assets API from Forge Back-End Functions”

  • Community thread: “Is it possible to query Assets using requestJira?”

@PatrickSifneos,

I am aware of the problems you’re reporting on all 3 of your asks: the routes, the requirement for a real user context, and the gaps in documentation. I’m personally working on the dev docs and working with the dev team to see what we can do about the others. I’m also eager that you could succeed in building an app without violating the Marketplace security guidelines about credential management.

While I acknowledge these areas, it would really help prioritization and resolution if you could be more specific about your integration expectations. For example, I have a working app and the scopes themselves do work. It’s the “without user context” that doesn’t. The fundamental assumption was that developers would be using the assetsImportType module with user context as described here:

Can you describe a bit more about the constraints on the app? Why does your scenario need asApp()?

@ibuchanan, thanks for the quick response and for acknowledging the gaps.

Happy to explain the constraints in detail. The app is not an import app — it reads from Assets outward, which changes the picture significantly relative to assetsImportType.

What the app does:
It syncs Assets objects (schemas, object types, attributes, all object records) into an external Neon Postgres database on a continuous basis. From there, users write SQL queries, build dashboard gadgets, and see live Assets data in issue panels and custom fields. Think of it as a read replica of Assets in SQL form.

Why user context is not an option for the core sync:

The sync runs in three invocation contexts that have no user session by design:

  1. Scheduled trigger — fires every 5 minutes to pick up incremental changes. There is no user. There is no way to attach a user session to a scheduled trigger in Forge.

  2. Web trigger — called by Jira Automation (HTTP action) for on-demand sync. Jira Automation fires this server-side with no user identity attached.

  3. Workflow post-function — fires on issue transitions. The Forge runtime invokes this with the event context, not a user OAuth token.

In all three cases, requestJira() in user context is simply unavailable — the Forge runtime does not provide a user token in those invocation types. api.asApp().requestJira() is the only option, and that’s exactly what returns 401 on the Assets API.

The assetsImportType module solves a different problem (importing third-party data into Assets via a user-initiated flow). Our problem is reading Assets data out in a background context without a user present.

To be concrete about the minimal ask: we need api.asApp().requestJira() to be able to call /jsm/assets/workspace/{id}/v1/objectschema/list (and similar read-only endpoints) with the CMDB read scopes declared in manifest.yml. This is the same pattern that works today for /rest/servicedeskapi/assets/workspace — which is how we get the workspace ID in the first place. The workspace lookup works as asApp(); the subsequent Assets API calls do not. That asymmetry is the core of the problem.

Kind Reagrds,

Patrick