Skip to main content

Atlassian products: multi-connector setup

This page is not about a single connector — it's about running one or more Atlassian connectors (today: identos.jira-user and identos.confluence) off a single Atlassian IdP.

It mirrors the Google Workspace setup page — same pattern, different vendor, a few Atlassian-specific quirks.

One IdP, many connectors​

identity_provider
└── atlassian (registered once, OAuth client_id/client_secret from
developer.atlassian.com)

managed_resource
├── jira-user → identos.jira-user + idp_passthrough + atlassian IdP
└── confluence → identos.confluence + idp_passthrough + atlassian IdP

A single Atlassian login grants access to every product whose connector is provisioned against that IdP. The user consents once, on the Atlassian consent screen, for the union of scopes every connector declares.

How scope growth works​

Each connector declares upstream_scopes on its idp_passthrough auth option, the Atlassian OAuth scopes its users need. Jira wants read:jira-user + read:jira-work + write:jira-work + read:me + offline_access. Confluence wants read:me, offline_access, 16 granular ...:confluence scopes and write:confluence-file; the Classic vs granular scopes section says why.

When you provision a managed resource in idp_passthrough mode, the gateway automatically merges that connector's upstream_scopes into the target IdP's scopes list (IdpScopeMerger). Subsequent user logins at /authorize pass the union of all those scopes to Atlassian.

before jira-user provision:       idp.scopes = [openid, email, profile]
after jira-user provision: idp.scopes = [openid, email, profile,
read:me, read:jira-user,
read:jira-work,
write:jira-work,
offline_access]
after confluence provision: idp.scopes = [... + read:space:confluence,
read:page:confluence,
write:page:confluence, ...,
write:confluence-file,
search:confluence]

The merge is additive and idempotent — uninstalling a connector does not remove scopes (another managed resource may still need them, and we can't safely attribute scopes to a single installer).

The audience=api.atlassian.com quirk​

Atlassian 3LO's /authorize endpoint requires an audience=api.atlassian.com query parameter. Without it, Atlassian returns a generic "There seems to be an issue with your identity provider" / server_error on id.atlassian.com.

The Atlassian IdP preset bundled with PBAC now auto-populates this via authorize_params when you pick Atlassian from the preset tile in Add identity provider. You can verify / override it in the IdP's Advanced → Extra authorize params textarea. Every Atlassian 3LO integration needs it — there is no supported setup without.

Classic vs granular scopes​

Atlassian has two scope styles for Cloud APIs:

  • Classic: read:jira-work, write:jira-work, read:me, read:confluence-content.all, etc. Single scope covers many API endpoints.
  • Granular: read:page:confluence, read:space:confluence, read:issue:jira, etc. Scope per resource × action.

Which style a connector ships depends on the upstream API it targets:

  • identos.jira-user uses classic scopes — the Jira REST v3 API accepts them and the route → scope mapping stays short.
  • identos.confluence uses granular scopes (read:page:confluence, write:page:confluence, read:space:confluence, …) because the Confluence v2 REST API (/wiki/api/v2/*) only accepts granular scopes — a token carrying the classic read:confluence-content.all is rejected by v2 endpoints with HTTP 401 "scope does not match". (The classic scopes still work on the legacy v1 API. The connector has five v1 routes, search_pages, add_label, remove_label, add_attachment and move_page, and its token carries the same granular scopes for every route.)

Atlassian's own guidance is to avoid mixing the two styles, but running Jira (classic) and Confluence (granular) off the same Atlassian IdP has been verified to work: the merged IdP scope list carries both sets, and Atlassian's consent screen accepts the union without looping. If you change a connector's scope style, update its manifest upstream_scopes and re-provision. The next install adds the new scopes to the IdP row and never removes the old ones. To stop requesting an old scope, exclude it under Upstream scope control on each resource that uses the IdP. Users re-consent on their next login.

Pre-configuring the Atlassian 3LO app​

On the Atlassian side, at https://developer.atlassian.com/console/myapps/:

  1. Create app → OAuth 2.0 (3LO). Name it something recognisable (PolicyArc gateway — prod).
  2. Authorization tab → set Callback URL to the AS callback:
    http://localhost:8080/oauth2/callback
    (or your real AS issuer + /oauth2/callback).
  3. Permissions tab → Add API for each Atlassian product you plan to connect. For Jira Cloud, add the Jira API; then click Configure on that row and grant every scope from the table below that matches a connector you'll provision.
  4. Settings tab → copy the Client ID and Secret into the PBAC admin UI (Add identity provider → Atlassian tile → paste into Step 3).

PBAC auto-merges scopes into its IdP row; it cannot teach Atlassian about scopes your app hasn't been granted. If the app's Permissions tab doesn't list write:jira-work, no amount of PBAC configuration will fix the consent-screen reject.

Per-product scope reference​

ProductOAuth scopes to grant (style noted per row)Shipped as a connector?
Jira Cloudread:me, read:jira-user, read:jira-work, write:jira-work, offline_accessidentos.jira-user
Jira Service Managementabove + read:servicedesk-request, write:servicedesk-request, manage:servicedesk-customerno
Confluence Cloud (granular, v2 API, plus the classic write:confluence-file for uploads)read:me, read:space:confluence, read:page:confluence, write:page:confluence, delete:page:confluence, read:blogpost:confluence, write:blogpost:confluence, read:folder:confluence, write:folder:confluence, delete:folder:confluence, read:comment:confluence, write:comment:confluence, delete:comment:confluence, read:attachment:confluence, write:confluence-file, read:label:confluence, write:label:confluence, search:confluence, offline_accessidentos.confluence
Bitbucket Cloudaccount, repository, repository:write, pullrequest, pullrequest:write, issue, offline_accessno
OpsGenieread:opsgenie, write:opsgenie, offline_accessno

Always cross-check against Atlassian's current scope documentation — Atlassian occasionally renames or adds scopes.

Bitbucket Cloud uses a different scope vocabulary from the rest of Atlassian; its scopes are verbs (repository, pullrequest:write, issue) rather than the verb:resource-product form. Don't mix Bitbucket's scope style with Jira/Confluence scopes in the same connector — they're granted separately on the Atlassian app, and the consent screen will surface them as distinct API blocks.

When a user's stored Atlassian token lacks a new scope​

If you install or provision an Atlassian connector after users have already signed in at Atlassian through PolicyArc, their stored Atlassian tokens do not carry the newly added scopes. Both Atlassian connectors run in idp_passthrough mode, so the gateway compares the stored token's granted scopes with the ones the connector requires, and the first call that finds one missing returns 401 (GatewayFlowService.java):

{"error": "idp_link_required", "idp": "atlassian", "link_url": "https://<your-environment>/gateway/link?ls=..."}

The message alongside it is "Connect your atlassian account to use this tool." A user who has never linked Atlassian gets the same response.

Two paths out:

  1. Recommended: the user opens the link_url and consents on Atlassian's screen. PolicyArc requests the expanded scope list. No operator action needed.
  2. Batch: delete the affected users' stored tokens with DELETE /admin/api/upstream-identity-tokens?subjectId=<sub>&idpId=<id>, where <id> is the IdP's numeric id from GET /admin/api/identity-providers, not its idpId string. A string gets 400. Their next gateway call returns the same link.

Going forward, the rule is: install and provision Atlassian connectors before inviting users to the gateway, and users never see this error.

Two managed resources, one connector​

Atlassian connectors today ship only in idp_passthrough mode — every call acts as the signed-in user, with that user's Atlassian permissions. There's no "service account" equivalent like Google's domain-wide-delegation because Atlassian Cloud doesn't expose one at the OAuth layer.

If a future use case needs a non-user identity (e.g. webhook ingest, batch reporting), expect a separate connector that uses Atlassian Connect or an API token with Basic auth rather than 3LO. This page doesn't cover those modes.

Troubleshooting​

SymptomLikely causeFix
id.atlassian.com/error?error=server_error on first redirectIdP row missing audience=api.atlassian.comEdit the IdP, paste audience=api.atlassian.com into Extra authorize params. The Atlassian preset should auto-fill this — if not, your IdP row predates the preset change
"There seems to be an issue with your identity provider" after entering credentialsScope list on the request includes a scope the app doesn't have on its Permissions tabAdd the missing scope on the Atlassian console → Permissions tab, or exclude it under Upstream scope control on the resource
Consent screen shows, user approves, then lands on AS with generic 400redirect_uri mismatchThe Authorization tab → Callback URL on Atlassian must match the AS's /oauth2/callback byte-for-byte, including scheme, port, and path
Refresh-token requests fail with invalid_grant after ~90 daysAtlassian rotates refresh tokens on every use; stored token is staleForce a re-link by deleting the user's stored token with DELETE /admin/api/upstream-identity-tokens, as above shows. Also ensure offline_access is in both the app's Permissions tab AND the connector's upstream_scopes
User logs in with Google, Jira tool says "no session", redirects back to Googleresource_type_to_idp mapping missing — Atlassian resource falls back to default_idpMap both resource types to the atlassian provider in the pbac.operator.controls.idp_selection row, as Route Atlassian resources to the Atlassian IdP shows. Installing the connector does not set this mapping.

Route Atlassian resources to the Atlassian IdP​

The mapping lives under resource_type_to_idp in the pbac.operator.controls.idp_selection row. This reads the row, adds both Atlassian types and writes it back, so a default_idp already in the row stays. Set POLICYARC_URL and POLICYARC_ADMIN_KEY as on Control data.

curl -fsS "$POLICYARC_URL/admin/api/policy-data" -H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" |
jq '([.[] | select(.dataKey == "pbac.operator.controls.idp_selection") | .payload | fromjson][0] // {})
| .resource_type_to_idp["urn:connector:identos:jira-user"] = "atlassian"
| .resource_type_to_idp["urn:connector:identos:confluence"] = "atlassian"
| {dataKey: "pbac.operator.controls.idp_selection", payload: .}' > idp_selection.json
curl -fsS -X PUT "$POLICYARC_URL/admin/api/policy-data/by-key?dataKey=pbac.operator.controls.idp_selection" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" -H 'Content-Type: application/json' -d @idp_selection.json

We ran it on a test instance and read the row back with both types mapped to atlassian.

See also​