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-useruses classic scopes — the Jira REST v3 API accepts them and the route → scope mapping stays short.identos.confluenceuses 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 classicread:confluence-content.allis 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_attachmentandmove_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/:
- Create app → OAuth 2.0 (3LO). Name it something recognisable
(
PolicyArc gateway — prod). - Authorization tab → set Callback URL to the AS callback:
(or your real AS issuer +
http://localhost:8080/oauth2/callback/oauth2/callback). - 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.
- 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
| Product | OAuth scopes to grant (style noted per row) | Shipped as a connector? |
|---|---|---|
| Jira Cloud | read:me, read:jira-user, read:jira-work, write:jira-work, offline_access | identos.jira-user |
| Jira Service Management | above + read:servicedesk-request, write:servicedesk-request, manage:servicedesk-customer | no |
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_access | identos.confluence |
| Bitbucket Cloud | account, repository, repository:write, pullrequest, pullrequest:write, issue, offline_access | no |
| OpsGenie | read:opsgenie, write:opsgenie, offline_access | no |
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:
- Recommended: the user opens the
link_urland consents on Atlassian's screen. PolicyArc requests the expanded scope list. No operator action needed. - Batch: delete the affected users' stored tokens with
DELETE /admin/api/upstream-identity-tokens?subjectId=<sub>&idpId=<id>, where<id>is the IdP's numericidfromGET /admin/api/identity-providers, not itsidpIdstring. A string gets400. 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
| Symptom | Likely cause | Fix |
|---|---|---|
id.atlassian.com/error?error=server_error on first redirect | IdP row missing audience=api.atlassian.com | Edit 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 credentials | Scope list on the request includes a scope the app doesn't have on its Permissions tab | Add 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 400 | redirect_uri mismatch | The 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 days | Atlassian rotates refresh tokens on every use; stored token is stale | Force 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 Google | resource_type_to_idp mapping missing — Atlassian resource falls back to default_idp | Map 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
- Jira connector — manifest reference + routes
- Google Workspace setup — the equivalent page for Google, same pattern
- MCP clients — how Claude Code and other MCP clients attach to the gateway
- Atlassian: OAuth 2.0 (3LO) apps
- Atlassian: scopes for OAuth 2.0 (3LO)
Send feedback about this page