Authentication/Scope issues when trying to query GraphQL from a resolver

In our Forge app, we want to check if its companion app is also installed on the current site. With the help of this thread: Using GraphQL API: Get all installed plugins in jira cloud instance, my colleague was able to come up with the following resolver function:

import api from "@forge/api";
import { JSM_APP_ID } from "../../../../client/utils/constants";

export default async (_payload, context) => {
  const query = /* GraphQL */ `
    query GetInstalledJSMPlugins($contexts: [ID!]!, $appIds: [ID!]!) {
      ecosystem {
        appInstallationsByContext(
          filter: {
            appInstallations: { contexts: $contexts }
            apps: { ids: $appIds }
          }
          after: null
        ) {
          nodes {
            id
            createdAt
            license {
              active
              type
              supportEntitlementNumber
              ccpEntitlementId
              ccpEntitlementSlug
              trialEndDate
              isEvaluation
              subscriptionEndDate
              billingPeriod
            }
            app {
              contactLink
              createdBy {
                name
              }
              description
              id
              marketplaceApp {
                appKey
                name
                tagline
              }
              name
              privacyPolicy
              termsOfService
              vendorName
            }
            appEnvironment {
              key
              type
              oauthClient {
                clientID
              }
            }
            appEnvironmentVersion {
              id
              version
              requiresLicense
              isLatest
              permissions {
                scopes {
                  key
                }
              }
            }
          }
          pageInfo {
            endCursor
            hasNextPage
          }
        }
      }
    }
  `;

  const variables = {
    contexts: [context.installContext],
    appIds: [`ari:cloud:ecosystem::app/${JSM_APP_ID}`],
  };

  const response = await api.asUser().requestGraph(query, variables);

  if (!response.ok) {
    throw new Error("Request failed");
  }

  console.log(JSON.stringify(await response.json(), null, 2));
};

When using the query in the GraphQL explorer on our sites it works as expected. However, when actually invoking the resolver, we get the following errors:

INFO    18:24:19.373  b6f1d4c1-e835-4271-8d02-89981b010986  {
  "errors": [
    {
      "message": "Auth category: THIRD_PARTY_OAUTH is not allowed in service cs_apps",
      "locations": [],
      "extensions": {
        "allowedAuth": [
          "SESSION",
          "API_TOKEN",
          "UNAUTHENTICATED"
        ],
        "presentedAuth": "THIRD_PARTY_OAUTH",
        "errorSource": "GRAPHQL_GATEWAY",
        "statusCode": 403,
        "agg": {
          "severity": "NORMAL",
          "ugcPiiSafe": true
        },
        "classification": "IncorrectAuthException"
      }
    },
    {
      "message": "This request does not contain the right authorisation scopes to access this field",
      "locations": [],
      "path": [
        "ecosystem",
        "appInstallationsByContext",
        "nodes",
        "app",
        "createdBy"
      ],
      "extensions": {
        "requiredScopes": [
          "identity:atlassian-external"
        ],
        "providedScopes": [
          "manage:jira-configuration",
          "read:cmdb-object:jira",
          "read:jira-work",
          "read:permission:jira",
          "offline_access",
          "write:jira-work",
          "read:jira-user"
        ],
        "errorSource": "GRAPHQL_GATEWAY",
        "statusCode": 403,
        "agg": {
          "severity": "NORMAL",
          "ugcPiiSafe": true
        },
        "classification": "InsufficientOAuthScopes"
      }
    }
  ],
  "data": {
    "ecosystem": null
  },
  "extensions": {
    "gateway": {
      "request_id": "3b79aba4439340fcb5e5a2572a85e19a",
      "crossRegion": false,
      "edgeCrossRegion": false
    }
  }
}

According to the GraphQL schema docs, the appInstallationsByContext field should be available to unauthenticated users. It mentions something about having “read permissions for each context”, though I wasn’t able to figure out what that actually means in this situation.

For the first error, I have tried sending {Authorization: UNAUTHENTICATED} as a header, but it just resulted in a different authentication error - unsurprising, as it was a complete guess.

For the second error, I have tried adding identity:atlassian-external as a scope to our manifest. This makes the app deployment fail at the linting stage, which matches what little info I was able to find about that scope elsewhere.

Could anyone share how to make this work, please?

@ViktorWolf1,

You won’t be able to become unauthenticated when using api.asUser().requestGraph(). The headers are manipulated by that function and can’t be overridden.

The scope identity:atlassian-external is a dead-end. It basically means the resource you’re trying to access is not available outside Atlassian.

Could you please open a “suggestion” issue on Jira (JAC) in the ECO project? Once you have the issue key, please let us know here so other folks can watch, vote, and comment.

Unfortunately, I doubt that would address your need on any short-term horizon. Forge doesn’t really have any explicit tools to manage “app dependencies” (ie a companion app). Is there a more direct way that 1 app could check for the “interface” of the other? Or is there a licensing concern?

@ibuchanan

Hi, sorry for a delayed response, I got a bit distracted :smiley:

Currently, our apps can communicate with each other through webtriggers, but this needs to be configured first by copying the webtrigger URL from one app to the other. This configuration workflow was actually one of the places where we wanted to check for the apps’ installation status - users might get confused where they’re supposed to copy the URL when they don’t actually have the other app installed. If we could check for it, we could display a warning for that specific situation and steer users toward the Marketplace listing of the other app.

I’ll create a suggestion soon and link it here. Thanks for your help.

@ViktorWolf1,

Thanks for explaining. Before that, I was wondering if one app could just make a request on the other app. If the request fails, there’s no app. But I see your “cart before the horse” problem now.

With that in mind, I wonder if there isn’t a deeper feature request than just “can my app ask if another app exists”. While that might work for your case, I can see even better potential solutions (easy to say when I wouldn’t be involved in building). For example, it seems like it would be better if Forge handled the exchange of events. For one, it means better leverage of the existing model established by product events. Maybe apps would specify the events they publish in the app manifest. For another, if Forge could handle the auth between apps, we could avoid a proliferation of webtrigger auth strategies, and a “tragedy of the anti-commons” for getting apps to talk to each other. All that to say, webtriggers seem like an awkward way for apps to talk to each other.

1 Like

@ibuchanan ,

Looking back, there were several points where we had to get creative, which ultimately lead to this issue.

Firstly, our app is usable without Service Management, but we wanted to provide optional integration with it. However, putting service desk scopes in the manifest made the app unavailable to users without Service Management product access. This is the reason why we split the app - one part handles base functionality, while the other is for JSM. This issue is already being tracked here https://jira.atlassian.com/browse/ECO-87

Then we found out the JSM part needed some data managed by the main part. The only way to achieve communication between them seemed to be webtriggers. I don’t really mind having to use webtriggers for this, but I think they could use some fleshing out. As you have mentioned, I believe they should have some built-in authentication (or there should at least be an official guide on implementing reliable auth of some kind). Webtriggers could also use a way to be discovered from outside the app that defines them, though that obviously has security implications, especially now that they essentially rely on security-through-obscurity.

That being said, a system designed specifically for inter-app communication would likely be better than webtriggers.

Finally, we wanted to make the process of manually pairing the apps a bit simpler when we ran into this latest problem. In our case it’s not the end of the world, but it feels like being able to find out what apps are installed is something that should be available. This seems to be available for Connect apps via the UPM API, so why not Forge?

Here’s the ECO issue you asked me to create: https://jira.atlassian.com/browse/ECO-370

2 Likes