Customer-managed remote: fetch() rejects consented endpoint,invokeRemote() overrides my Authorization header — how do I call acustomer-chosen atlassian.net site with my own Basic Auth?


I’m building a Forge app (Jira global page, Custom UI) that needs to call
a second, admin-chosen, Jira Cloud site’s REST API using Basic Auth
(service-account email + API token entered by the admin) — not the site
the app is installed on.

Manifest (relevant parts):

permissions:
  external:
    configurable:
      enabled: true
    fetch:
      backend:
        - remote: target-site
remotes:
  - key: target-site
    baseUrl: "https://placeholder.atlassian.net"
    operations:
      - fetch
    configurable:
      name: Target Jira site
      description: "The Jira Cloud site that receives synced issues."
      supportedPatterns:
        - "*.atlassian.net"

Frontend consent flow (Custom UI, @forge/bridge):

await permissions.remote.set({
  remotes: [{ key: 'target-site', configured: { endpoint: targetSiteUrl } }],
});

permissions.remote.get({ keys: ['target-site'] }) correctly confirms the
endpoint is stored, e.g.:

[{"key":"target-site","configured":{"endpoint":"https://my-target.atlassian.net"}}]

Attempt 1 — plain fetch() from @forge/api to the literal consented URL:

await fetch(`${targetSiteUrl}/rest/api/3/myself`, {
  headers: { Authorization: `Basic ${auth}` },
});

Always fails, immediately and even after several minutes (ruled out
propagation delay):

403
URL not included in the external fetch backend permissions: https://my-target.atlassian.net.
Visit go.atlassian.com/forge-egress for more information.

…despite permissions.remote.get confirming the consent is stored for
exactly that URL, and the manifest referencing the remote under
fetch.backend.

Attempt 2 — invokeRemote() instead:

await invokeRemote('target-site', {
  path: '/rest/api/3/myself',
  method: 'GET',
  headers: { Authorization: `Basic ${auth}` },
});

This one does reach the target site (no more egress 403) but the target
responds 401 Client must be authenticated to access this resource. Per
your own docs (“Forge Remote essentials”),
the authorization header is always attached by Forge as a required
header carrying the Forge Invocation Token (FIT) as a bearer token — which
appears to silently override whatever Authorization header I set myself,
so my Basic Auth credentials never reach the target.

The question is: is there a supported way to make a plain fetch() honor a
customer-managed remote’s runtime-configured (admin-consented) endpoint,
so I can set my own Authorization header? Or is invokeRemote +
“host your own backend that verifies the FIT and does the real outbound
call” the only supported pattern for this — i.e. is calling a third
party’s
API (in this case, another Atlassian Cloud site) directly with
app-supplied credentials via a customer-managed remote simply not
supported today, and a self-hosted proxy is required?

Forge CLI 13.5.0, @forge/api 8.0.5, @forge/bridge 6.3.1.

Regardless of technical feasibility, you are not allowed to use Basic Auth for Cloud sites: Basic auth for REST APIs

Remotes are a specific kind of server to exchange to, and Atlassian sites aren’t really meant for that. I think you’re just looking for a custom egress https://developer.atlassian.com/platform/forge/customer-managed-egress-and-remotes/#customer-managed-egress-groups and depending on “where” you execute your call either use FETCH_BACKEND_SIDE or FETCH_CLIENT_SIDE. I’m not sure what would work to handle authentication though

Hey @MarcoGiachin, echoing the above. A remote is generally a server defined as part of your app, not another Atlassian site.

permissions.remote.get({ keys: ['target-site'] }) correctly confirms the
endpoint is stored, e.g.:

Even though the endpoint is stored, this doesn’t mean that the endpoint will work. You could put in a non-existing URL, and as long as it conforms to a URL structure it will be saved. When the URL is invoked in the flow of the app is when it’s called.

A customer-managed remote is essentially a manifest defined remote under the hood, it’s URL can just be configured in the app. This means that any rules that apply to a regular remote will also apply to a customer-managed one.

Likely the easiest approach would be the self-hosted proxy approach you’ve mentioned at the bottom of your post.