Expose Forge app REST APIs - scope does not match

Hi,

I have been trying to expose custom REST API endpoints in my Forge app for some time now, and I can’t figure out how to troubleshoot the issue.

I have followed this documentation step by step: https://developer.atlassian.com/platform/forge/access-rest-apis-exposed-by-a-forge-app/

manifest.yml:

modules:
  jira:globalPage:
    - key: forge-test-rest-hello-world-full-page
      resource: main
      resolver:
        function: resolver
      title: forge-test-rest
      routePrefix: hello-world
  function:
    - key: resolver
      handler: index.handler
    - key: handler1
      handler: index.handler1
    - key: handler2
      handler: index.handler2 

  apiRoute:
    - key: employee-api-1
      path: /employeeName
      operation: GET
      function: handler1
      accept:
        - application/json 
      scopes:
        - read:employee:custom
    - key: employee-api-2
      path: /employeeName
      operation: POST
      function: handler2
      accept:
        - application/json 
      scopes:
        - read:employee:custom
        - write:employee:custom

resources:
  - key: main
    path: static/hello-world/build
app:
  runtime:
    name: nodejs24.x
    memoryMB: 256
    architecture: arm64
  id: ari:cloud:ecosystem::app/xxxxxxx

custom-scopes.yaml

version: 1
scopes:
  read:employee:custom:
    displayName: Read Employee Info
    description: >-
      Read employee information such as name, date of joining, etc.
  write:employee:custom:
    displayName: Edit Employee Info
    description: >-
      Edit information related to an employee such as name, dob, payroll info, etc.

I have created a new OAuth 2 app in the developer portal and assigned the right scopes:

After generating the refresh token, I then proceeded in getting an access token (following this doc: https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/#2--exchange-authorization-code-for-access-token)

Using this access token, calls made to <jira_api>/oauth/token/accessible-resources return an empty array.

Any API call to my forge app endpoints result in unauthorized:

https://<jira_instance>.atlassian.net/gateway/api/svc/jira/apps/<forgeAppId>_<forgeEnvId>/getEmployeeName

Result:

{
    "code": 401,
    "message": "Unauthorized; scope does not match"
}

How can I figure out the issue here? I get the same very unhelpful message no matter what I try (even changing the URL randomly to trigger a 404 results in 401).

Thanks!

Same here.
I have been following the page here: https://developer.atlassian.com/platform/forge/access-rest-apis-exposed-by-a-forge-app/

I am testing on my development site, when I run forge custom-scopes list, I can see my custom scopes present.

I copied the URL from the 3LO app → Authorization → Authorization URL generator to browser, attempting to exchange the authorization code for access token. A response with status 200 and an access token were returned with expiry time of 3600 seconds, however, when I check the accessible resources, I got an empty array, and reaching for the rest api endpoints returns a 401 Unauthorized: scope does not match.

I tried using curl and the following script, but both failed.

const exchangeAuthorzationCodeForAccessToken = async (code) => {
  const resp = await fetch('https://auth.atlassian.com/oauth/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      client_id: clientId,
      client_secret: clientSecret,
      code,
      redirect_uri: 'http://localhost/callback',
    }),
  });
  if (!resp.ok) {
    throw new Error(`Failed to exchange authorization code for access token: ${resp.statusText}`);
  }
  const data = await resp.json();
  console.log('data', JSON.stringify(data, null, 2));
  return data.access_token;
}

const checkAccessibleResources = async (accessToken) => {
  const resp = await fetch('https://api.atlassian.com/oauth/token/accessible-resources', {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Accept': 'application/json',
    },
  });
  if (!resp.ok) {
    throw new Error(`Failed to check accessible resources: ${resp.statusText}`);
  }
  const data = await resp.json();
  console.log('data', JSON.stringify(data, null, 2));
  return data;
}

const testApi = async (accessToken) => {
  const resp = await fetch(`${siteUrl}/gateway/api/svc/jira/apps/${appId}_${envId}/${path}`, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Accept': 'application/json',
    },
  });
  if (!resp.ok) {
    throw new Error(`Test failed: ${resp.statusText}`);
  }
  const data = await resp.json();
  console.log('data', JSON.stringify(data, null, 2));
  return data;
}


const main = async (code) => {
  try {
    const accessToken = await exchangeAuthorzationCodeForAccessToken(code);
    const permittedResources = await checkAccessibleResources(accessToken);
    await testApi(accessToken);
  } catch (error) {
    console.error('Error:', error);
  }
}

I can confirm that in the 3LO app → Permissions, I can see my Forge app present, and the scopes are selected; the REST API is also enabled from the site.

Thanks for your time!

Hi,

Atlassian support helped me troubleshoot the issue (they were quite responsive).

Basically the generated Authorization URL that I use to get the Refresh Token did not include the Jira scopes (read:forge-app:jira), only my custom scopes. I had to manually modify the URL to include it.

Also, the documentation was misleading (it was saying the endpoint is getEmployeeNameinstead of employeeName).

My only concern now is that it seems the refresh token has a very short lifespan (5 minutes), so I don’t see how this whole flow could work.

I can share a bit more about the issue. I am trying to access my custom scopes, so I did not need to modify the URL. I also created a simple server to handle the code, so the expiry time does not bother me as well.

I created a simple server GitHub - michaelcychan/simple-server-to-test-atlassian-3lo-authorisation to exchange the code for authorization token, so it should not be a timeout issue. Here are the logs I got from the above server:

code received not empty: true
cloudId <cloud_id>
Access Token Response {
  "access_token": "<access_token>",
  "expires_in": 3600,
  "token_type": "Bearer",
  "scope": "<app_id>.<env_id>:read:feature:custom<app_id>.<env_id>:read:download:custom"
}
Access token not empty: true
Accessible resources: []
API call failed with status: 401
Response body: {"code":401,"message":"Unauthorized; scope does not match"}
API call failed with status: 401
Response body: {"code":401,"message":"Unauthorized; scope does not match"}

The scope listed in the Access Token Response are the ones that I have in the Forge app, the app_id and env_id are correct. I am sure the client id and client secret are correct as when I changed them, I even failed to obtain the access token.

The problem I have is that the Accessible resources are empty, hence the 3LO app is not authorised to access any of the exposed api.

Hi Michael,

All my issues are fixed thanks to Atlassian support, so I will try to summarize everything here.

Fix scopes in the Authorization URL

Assuming your Forge app is for Jira, you will need to add the following scopes in your 3LO App (Permissions tab):

  • Your custom scopes defined for your Forge app
  • read:forge-app:jira

In the Authorization tab, the “Marketplace App” Authorization URL will be incomplete (bug filed here: https://jira.atlassian.com/browse/ECO-1289). It will only contain your custom scopes, and will not include read:forge-app:jira

So you need to manually adjust this URL to add that scope.

Also, if you want to get a long refresh token (as opposed to the 5 minutes authorization code), you can also add the offline_access scope to your URL.

So your url should look like:

https://auth.atlassian.com/authorize?[something]scope=<customscopes>%20read:forge-app:jira%20offline_access[something]

Refresh tokens

If you have added the offline_access scope to your authorization url, you will now get a refresh token when you exchange your authorization code for an access token:

{
    "access_token": "...",
    "expires_in": 3600,
    "token_type": "Bearer",
    "refresh_token": "...",
    "scope": "..."
}

This refresh token should be valid for 90 days.

You can use it to generate a new access token (https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/#how-do-i-get-a-new-access-token--if-my-access-token-expires-or-is-revoked-)

Available resources

I was trying to use this call to validate that my settings were correct:

https://api.atlassian.com/oauth/token/accessible-resources

However, this call always returns an empty array for me. I’m still able to call my custom Forge endpoints though.

424 Failed Dependency

In case your custom REST API call results in a 424 Failed Dependency with error message:

Invocation failed for appId: unknown

It means your endpoint code has some issues, either an exception or a wrong return object.

In my case I was returning an object in ‘body’ as opposed to a ‘string’.

1 Like

Thanks so much, @muller.josselin ! I got mine working following your instructions.