OAuth 2.0 (3LO) tokens issued by auth.atlassian.com contain my requested granular Jira Software scopes (verified via JWT decode), but every call to
/rest/agile/1.0/* returns:
HTTP/1.1 401
{“code”:401,“message”:“Unauthorized; scope does not match”}
The same token works fine for Core API endpoints (/rest/api/3/.).
Any kind of guidance in this regard will help.
Thanks
I’m hitting the exact same issue. Forge Custom UI app calling out via @forge/bridge.invokeRemote to an external Forge Remote backend, forwarded x-forge-oauth-user token, manifest declares read:board-scope:jira-software (and read:jira-work, read:jira-user, read:sprint:jira-software, read:jql:jira).
Same token: GET /rest/api/3/project/{id} → 200 OK. Immediately after, GET /rest/agile/1.0/board?projectKeyOrId={id} → 401 {"code":401,"message":"Unauthorized; scope does not match"}.
Any update on this? Would help to know whether there’s an undocumented additional scope/requirement, or if this is specific to how Forge Remote forwards tokens versus api.asUser().requestJira() called from inside a Forge function.
Hey @MayankBaber and @JimMartin,
I’m not able to reproduce a general problem at the OAuth layer. The following script works for me and my registered OAuth 2 client:
jsw.sh
#!/usr/bin/env bash
# oauth2c is a CLI for doing the "OAuth dance"
# See: https://github.com/SecureAuthCorp/oauth2c
# oauth2c requires the callback URL to be: http://localhost:9876/callback
# Find the "Client ID" and "Secret" in the Settings
# for this app in the developer console.
# Either set these values in your environment,
# or uncomment & fill in the following:
ATLASSIAN_APP_3LO_CLIENT_ID='client-id'
ATLASSIAN_APP_3LO_CLIENT_SECRET='client-secret'
# This space separated list of scopes
# can be a subset or the complete set
# selected in the developer console.
# https://developer.atlassian.com/console/myapps
# SCOPES='offline_access read:me'
SCOPES='offline_access read:project:jira read:issue-details:jira read:jql:jira write:board-scope.admin:jira-software delete:board-scope.admin:jira-software read:board-scope.admin:jira-software write:board-scope:jira-software read:board-scope:jira-software write:epic:jira-software read:epic:jira-software write:issue:jira-software read:issue:jira-software write:sprint:jira-software delete:sprint:jira-software read:sprint:jira-software write:source-code:jira-software read:source-code:jira-software write:feature-flag:jira-software read:feature-flag:jira-software write:deployment:jira-software read:deployment:jira-software write:build:jira-software read:build:jira-software write:remote-link:jira-software read:remote-link:jira-software'
# Starts by loading a refresh token,
# if it exists.
# Some cases where manually deleting the file is needed:
# - Expired refresh token (usually a matter of months).
# - Changed the client id/secret pair or scopes.
REFRESH_TOKEN=$(jq --raw-output '.refresh_token' access_token_response.json)
if [ -z "$REFRESH_TOKEN" ]; then
echo "Performing authorization code flow to obtain initial access token response"
oauth2c https://auth.atlassian.com/ \
--client-id "${ATLASSIAN_APP_3LO_CLIENT_ID:?Missing client id}" \
--client-secret "${ATLASSIAN_APP_3LO_CLIENT_SECRET:?Missing secret}" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method client_secret_post \
--scopes "$SCOPES" \
--silent \
>access_token_response.json
else
echo "Performing refresh token flow to obtain a fresh access token response"
echo "If this step fails, try deleting 'access_token_response.json' to start with a fresh code flow"
oauth2c https://auth.atlassian.com/ \
--client-id "${ATLASSIAN_APP_3LO_CLIENT_ID:?Missing client id}" \
--client-secret "${ATLASSIAN_APP_3LO_CLIENT_SECRET:?Missing secret}" \
--grant-type refresh_token \
--auth-method client_secret_post \
--refresh-token "$REFRESH_TOKEN" \
--silent \
>access_token_response.json
fi
ACCESS_TOKEN=$(jq --raw-output '.access_token' access_token_response.json)
xhs https://api.atlassian.com/oauth/token/accessible-resources \
--auth-type bearer \
--auth "$ACCESS_TOKEN" \
>accessible_resources.json
CLOUD_ID=$(jq --raw-output '.[0].id' accessible_resources.json)
echo "From accessible resources, found cloud id: ${CLOUD_ID:?Missing cloud id from accessible resources}"
# xhs "https://api.atlassian.com/ex/jira/${CLOUD_ID}/rest/agile/1.0/issue/MOBL-9" \
# --verbose \
# --auth-type bearer \
# --auth "$ACCESS_TOKEN"
xhs "https://api.atlassian.com/ex/jira/${CLOUD_ID}/rest/agile/1.0/board" \
--verbose \
--auth-type bearer \
--auth "$ACCESS_TOKEN"
I don’t mean this as a “works on my machine” answer. The point of the above script is to provide a “minimum test case” to help isolate the problem. Hopefully, it helps you diagnose the problem by making it faster to try different developer console configurations (scopes, callbacks, etc), and to check the combination of any path with the specified scopes.
@JimMartin, in your more specific case, you’ve provided enough information to indicate you do have a scope problem. The Get all boards endpoint specifies read:project:jira is required. I confirmed with script above and this:
SCOPES='offline_access read:board-scope:jira-software read:project:jira'
Plus using the ?projectKeyOrId={id} parameter. I get a 200 response.
All that said, this wouldn’t be the first time that specs are wrong, so please do report a bug to developer support if you can prove a combination of scopes & path does not work as specified.