Skip to content

Handle postMessage from trusted popup of webchat:callURL - #5863

Merged
William Wong (compulim) merged 22 commits into
mainfrom
feat-call-url-return
Sep 25, 2026
Merged

William Wong (compulim) merged 22 commits into
mainfrom
feat-call-url-return

Conversation

@compulim

@compulim William Wong (compulim) commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Related to #5862.

Changelog Entry

Added

  • Added card action webchat:callURL and Adaptive Card action Action.OpenUrlDialog
    • The card action is designed to host popup for authentication and authorization (call-and-return pattern), it can optionally send a postback message to the bot
    • Open in popup, in PR #5862, by @compulim
      • Added styleOptions.callURLActionPopupWindowHeight/Width for sizing the popup window
      • URL must be absolute with scheme of either http:// or https://
      • Reference payload for Direct Line webchat:callURL card action can be found in this test
      • Reference payload for Adaptive Card Action.OpenUrlDialog can be found in this test
      • Note: the Adaptive Card implementation is based on observation of how other apps behave and could deviate from their official implementation
      • Adaptive Card: dialogHeight, dialogTitle, and dialogWidth are ignored, use styleOptions.callURLActionPopupWindowHeight/Width for dialog sizing instead
    • Trusted popup can send postback message, in PR #5863, by @compulim
      • Popup window can be opened as trusted or untrusted based on their origin
        • Trusted popup will have access to window.opener and can send postback value
        • Untrusted popup will be opened with noopener noreferer and they cannot send postback value
      • Same origin is always trusted, multiple cross origins can be trusted via the new styleOptions.callURLActionTrustedOrigin style option
      • Content in trusted popup could potentially access data and manipulate the page in the origin where Web Chat is hosted. Content must be well-maintained and frequently audited. In a trusted popup, never redirect to an untrusted cross origin
      • To send a postback value, call window.opener.postMessage({ type: 'postback', value: {} | string }, '...')
        • Postback is only accepted within 5 minutes after the popup window is opened and from a trusted origin
        • Each popup window can only send atmost one postback, subsequent postbacks are ignored
        • replyToId will be automatically filled in by the ID of the originating activity

Description

This is an extension to #5862.

Design

Reliability of postMessage

Despite we are opening a URL that we trust, postMessage can still fail in the following cases, and not limited to these cases:

  • Popup blocker intervened
    • Despite the popup can be manually allowed by C2, the popup will no longer have access to opener object, which means the popup will not be able to send MessageEvent to Web Chat
  • Cross-Origin-Opener-Policy (a.k.a. COOP) could one-sidedly impact the opener object
    • If COOP is enforced, the returning Window object from the window.open call could set to closed to true asynchronously (a.k.a. sever) and Web Chat will stop listening to "message" event because the popup is consider closed

Return the value on client-side is only best effort. To push for 100% reliability, bot developer should return the value on the service side.

Resource management

Rationale:

  • addEventListener('message') is taking some resources, we should not keep it forever
    • removeEventListener('message') should be called when the event listener is no longer needed
  • MessageEvent listener is no longer needed, when:
    • The popup is closed: no more MessageEvent will be dispatched
    • The value has been returned: we only handle the very first MessageEvent
      • This is regardless whether the MessageEvent.data has valid or invalid data, we will remove event listener in both cases
  • To detect if the popup is closed, the only method is to periodically poll window.open().closed property, which is very expensive operation

Thus, we only listen to MessageEvent for 5 minutes after the popup is open. After 5 minutes, we will remove the closed property check and assume the dialog is staled/closed.

Web security: cross origin

window.opener property

We are defining what popups are safe to communicate with Web Chat and what are not (trusted vs. untrusted). This is because the trusted popup will have access to the opener object for postMessage. However, it also have some access to the parent Window object, which means the popup can replace the parent page by calling opener.location.replace('...').

Trusted popup means the URL opened in the popup is either:

  • Same origin, or
  • Cross origin but in the allow list defined in the style options

Web developer must explicitly allowlisting the URL to opt into the trusted popup feature to prevent rogue bot from randomly opening any URLs.

Web developer must also make sure the trusted popup should not redirect to other untrusted URLs.

To trust an URL, put it inside the new style option named callURLActionTrustedOrigin.

Redirection in the popup

When a MessageEvent is received, Web Chat will check if the event.origin is from an origin we trust in style options or same origin.

It is not necessarily that the event.origin match the opening origin. The popup can redirect to other origins before requesting for a post back.

Notes to UI implementors

Note: web security is a broad topic and we cannot fully illustrates in this section. When implementing this feature yourself, please consult with your web security team.

This section is for implementors who are working on a similar feature with their own web UI.

Showing the content as a popup

  • When rendering the Adaptive Card, handle the Action.OpenUrlDialog call
    • Bring-your-own Adaptive Card renderer
      • Official spec at https://adaptivecards.microsoft.com/?topic=Action.OpenUrlDialog
      • Minimally handle the url property with an absolute URL
      • When the Adaptive Card action is triggered, use window.open() call with features of "popup" to show the content as a popup
      • Omit noopener and noreferrer, the popup need to communicate with your chat UI later
      • Make sure the URL is pointing to a trusted site, the content of the site could "escape" the popup and impact the security of your website
    • Official Adaptive Card JavaScript SDK
      • Action.OpenUrlDialog is not implemented in the official SDK yet
      • Extends the SDK by using SerializationContext.actionRegistry to register the new Action.OpenUrlDialog action
      • For details, please refer to Add Web Chat-specific webchat:callURL action #5862 and look at the changes at useParseAdaptiveCardJSON.ts on how to extend AC SDK
      • Follow the rest of the steps in "bring-your-own Adaptive Card renderer" section

Notes:

  • Only open trusted URLs without "noopener noreferrer"
    • In other words, all untrusted URLs must be opened with "noopener noreferrer"
  • Content from the trusted URL must not perform redirection to an untrusted URL
  • Adaptive Card SDK is extensible thru SerializationContext and it can cover our scenario

Handling response from the popup window

The popup window can request Web Chat to send a "post back" message to the agent with a specific value. The popup window use the following code to communicate with Web Chat.

window.opener?.postMessage({
  type: 'postback',
  value: { hello: 'World!' } // or "Hello, World!"
}, '<origin-domain-of-web-chat>');
  • Attach a message event handler
  • After calling window.open(), retains the return value, which is a Window object
    • The value can be null because of various reason, including but not limited to web security and popup blocker
  • In the message event handler, filter out MessageEvent originated from the popup:
    1. MessageEvent.source must be same instance as the Window object of the popup window
    2. MessageEvent.origin must be a trusted origin
    3. MessageEvent.data must be in the format mentioned above
  • Send the MessageEvent.data.value via the "postback" mechanism
  • After the first MessageEvent is handled, ignore subsequent MessageEvent
    • Only one response is allowed for every popup window

Notes:

  • window.open() may return null
    • In this case, do not handle message event, we have no way to verify the legitimacy of the MessageEvent
  • Do not handle the message event after the popup window is closed
    • Window.closed property could become true when Cross-Origin-Opener-Policy is set
  • The popup window should close itself or thru user gesture, chat UI should not handle the lifecycle of the popup window
  • The popup window may close itself without any response
  • The popup window could redirect to an untrusted URL, do not handle the response from untrusted URL
  • Handling of popup response is "best effort" and not a guarantee, agent should provide another mechanism in case the handling fail silently

Specific Changes

  • Added style option named callURLActionTrustedOrigin (delimited by comma)
  • For trusted popup, listen to MessageEvent and postback as directed
  • I have added tests and executed them locally
  • I have updated CHANGELOG.md
  • I have updated documentation

Review Checklist

This section is for contributors to review your work.

  • Accessibility reviewed (tab order, content readability, alt text, color contrast)
  • Browser and platform compatibilities reviewed
  • CSS styles reviewed (minimal rules, no z-index)
  • Documents reviewed (docs, samples, live demo)
  • Internationalization reviewed (strings, unit formatting)
  • package.json and package-lock.json reviewed
  • Security reviewed (no data URIs, check for nonce leak)
  • Tests reviewed (coverage, legitimacy)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical and moderate issues affect compilation, dispatch, security, and reply correlation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 High severity · 2 Medium severity · 2 Low severity

Open (7)
What changed in this PR

Adds trusted popup postMessage handling for webchat:callURL, including postback correlation and cross-origin coverage.

Changes:

  • Adds trusted-origin validation, popup cleanup, and security configuration.
  • Propagates replyToId through card actions and postback APIs.
  • Adds popup lifecycle, validation, timeout, and cross-origin tests.
File Description
serve-test.json Adds popup-related headers.
packages/​fluent-theme/​src/​components/​suggestedActions/​SuggestedAction.tsx Updates suggested-action props.
packages/​core/​src/​sagas/​sendPostBackToPostActivitySaga.ts Includes replyToId in postback activities.
packages/​core/​src/​index.ts Exports postback initialization typing.
packages/​core/​src/​actions/​sendPostBack.ts Adds optional postback initialization.
packages/​component/​src/​Middleware/​CardAction/​createCoreMiddleware.ts Handles trusted popup messages and cleanup.
packages/​bundle/​src/​adaptiveCards/​createAdaptiveCardsAttachmentMiddleware.tsx Passes activity IDs to Adaptive Cards.
packages/​bundle/​src/​adaptiveCards/​createAdaptiveCardsAttachmentForScreenReaderMiddleware.tsx Passes activity IDs to accessible cards.
packages/​bundle/​src/​adaptiveCards/​Attachment/​AdaptiveCardRenderer.tsx Propagates IDs for card actions.
packages/​bundle/​src/​adaptiveCards/​Attachment/​AdaptiveCardContent.tsx Forwards replyToId.
packages/​bundle/​src/​adaptiveCards/​Attachment/​AdaptiveCardAttachment.tsx Supports replyToId props.
packages/​api/​src/​types/​CardActionMiddleware.ts Extends card-action middleware types.
packages/​api/​src/​StyleOptions.ts Adds trusted-origin configuration.
packages/​api/​src/​hooks/​useSendPostBack.ts Exposes postback initialization.
packages/​api/​src/​hooks/​middleware/​createDefaultCardActionMiddleware.ts Correlates postback actions.
packages/​api/​src/​hooks/​internal/​WebChatAPIContext.ts Updates API context typing.
packages/​api/​src/​hooks/​Composer.tsx Passes card-action initialization.
packages/​api/​src/​defaultStyleOptions.ts Defines trusted-origin defaults.
docker-compose-wsl2.yml Adds a cross-origin test alias.
CHANGELOG.md Documents trusted popup behavior.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​timeout/​test.html Tests listener timeout behavior.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​timeout/​dialog.skip.html Provides timeout popup content.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​simple/​test.html Tests successful popup postbacks.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​simple/​dialog.skip.html Provides successful popup content.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​postMessageTwice/​test.html Tests duplicate postback suppression.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​postMessageTwice/​dialog.skip.html Provides duplicate-message content.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​popupBlocker/​test.html Tests blocked popups.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​invalidPayload/​test.html Tests invalid popup payloads.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​invalidPayload/​dialog.skip.html Provides invalid-message content.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​crossOrigin/​untrusted/​test.html Tests untrusted cross-origin popups.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​crossOrigin/​untrusted/​dialog.skip.html Provides untrusted popup content.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​crossOrigin/​trusted/​test.html Tests trusted cross-origin postbacks.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​crossOrigin/​trusted/​dialog2.skip.html Provides trusted postback content.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​crossOrigin/​trusted/​dialog1.skip.html Tests trusted popup navigation.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​crossOrigin/​failOnPostMessage/​test.html Tests rejected cross-origin messages.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​crossOrigin/​failOnPostMessage/​dialog2.skip.html Provides rejected postback content.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​crossOrigin/​failOnPostMessage/​dialog1.skip.html Tests rejected popup navigation.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​closePopup/​test.html Tests cleanup after popup closure.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialogReturn/​closePopup/​dialog.skip.html Provides closing popup content.
__tests__/​html2/​adaptiveCard/​openUrlDialog/​dialog.skip.html Updates the base popup fixture.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/component/src/Middleware/CardAction/createCoreMiddleware.ts Outdated
Comment thread packages/core/src/actions/sendPostBack.ts Outdated
Comment thread packages/api/src/types/CardActionMiddleware.ts Outdated
Comment thread CHANGELOG.md Outdated
Comment thread packages/component/src/Middleware/CardAction/createCoreMiddleware.ts Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Make replyToId optional in CardActionMiddleware type

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Corrected grammar and clarified postback limitations in the changelog.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@compulim
William Wong (compulim) merged commit bdb243c into main Sep 25, 2026
86 of 88 checks passed
@compulim
William Wong (compulim) deleted the feat-call-url-return branch September 25, 2026 04:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants