Forge external auth completes successfully, but outboundAuth/finish tab does not close and UI shows “auth window closed”

Forge external auth completes successfully, but outboundAuth/finish tab does not close and UI shows “auth window closed”

Hi,

I’m seeing an issue with Forge external authentication and I’d like to know whether anyone else has run into something similar.

I have a Forge Jira issue panel using an OAuth2 provider configured against Microsoft Entra ID. The app requests an access token for my own backend API, not Microsoft Graph.

The provider is configured roughly like this:

providers:
  auth:
    - key: microsoft-backend
      name: Microsoft Backend
      type: oauth2
      clientId: <client-id>
      scopes:
        - openid
        - profile
        - email
        - api://<client-id>/access_as_user
      remotes:
        - portal-backend
      bearerMethod: authorization-header
      actions:
        authorization:
          remote: microsoft-login
          path: /<tenant-id>/oauth2/v2.0/authorize
        exchange:
          remote: microsoft-login
          path: /<tenant-id>/oauth2/v2.0/token
        refreshToken:
          remote: microsoft-login
          path: /<tenant-id>/oauth2/v2.0/token
        retrieveProfile:
          remote: portal-backend
          path: /api/auth/profile
          resolvers:
            id: id
            displayName: displayName

remotes:
  - key: microsoft-login
    baseUrl: https://login.microsoftonline.com
  - key: portal-backend
    baseUrl: https://<my-backend-domain>

My backend validates the Microsoft JWT using Microsoft JWKS and checks:

signature
exp / nbf / iat
aud = api://<client-id>
tid / issuer
scp contains access_as_user

The retrieveProfile endpoint is intentionally simple. Forge calls it with the bearer token, my backend validates the token, extracts claims from it, and returns:

{
  "id": "<oid-or-sub>",
  "displayName": "<name-or-email>",
  "email": "<email-or-upn>",
  "tenantId": "...",
  "objectId": "..."
}

From the backend logs, everything looks successful:

JWT valid
aud=api://<client-id>
scope=access_as_user
GET /api/auth/profile
[AUTH PROFILE] Returning Forge profile id=... displayName=...

Also, if I manually refresh the Jira issue panel after the auth flow, the app correctly detects that the user is authenticated. So the credentials seem to be stored by Forge.

The problem is that the auth browser window/tab remains stuck on:

https://id.atlassian.com/outboundAuth/finish?code=...&state=...

and the Jira UI shows a Forge banner/message along the lines of “auth window closed”. It looks like the OAuth flow has actually completed, but Forge does not close the outboundAuth/finish window or notify/resume the panel cleanly.

I have added polling in the issue panel after calling requestCredentials(), so the panel can detect hasCredentials() eventually, but the Forge-auth window behavior still looks broken/confusing for the user.

Has anyone seen this with Forge external auth, especially with a custom backend API scope like:

api://<client-id>/access_as_user

rather than Microsoft Graph scopes like User.Read?

Is there anything specific required in the retrieveProfile response or provider configuration to make outboundAuth/finish close reliably? Or is this a known limitation/bug depending on browser, iframe, popup handling, or environment?

Any pointers would be appreciated.

Hi @FranciscoRomeroRuiz Could you please share the traceId from your browser where its calling the outboundAuth/finish API, it would help in debugging if the retrieveProfile endpoint is working correctly or not ?

Hi Divyansh,

Sure, I can try to provide it.

Could you please clarify where exactly I should get that traceId from?

Do you mean a traceId returned in the response body or headers of the browser request to:

https://id.atlassian.com/outboundAuth/finish

I checked the OAuth redirect URL, but I only see the code and state query parameters there. I want to make sure I’m looking in the right place and not sharing the OAuth code/state by mistake.

Thanks.

@FranciscoRomeroRuiz It should be present in the headers on the same page where you are seeing the state