Skip to content

Add Nested App Authentication (NAA) bridge for MSAL v5 extensions#138

Merged
mihaelams1 merged 10 commits into
masterfrom
users/mkorte/naa-bridge-for-extensions
Jun 9, 2026
Merged

Add Nested App Authentication (NAA) bridge for MSAL v5 extensions#138
mihaelams1 merged 10 commits into
masterfrom
users/mkorte/naa-bridge-for-extensions

Conversation

@mihaelams1

@mihaelams1 mihaelams1 commented May 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Nested App Authentication (NAA) support to the Azure DevOps extension SDK, enabling extensions to use MSAL v5's createNestablePublicClientApplication for authentication in cross-origin iframes.

Problem

Extensions running in cross-origin iframes cannot use MSAL v5's BroadcastChannel-based redirect flow due to browser storage partitioning -- the popup and iframe cannot communicate across partitions.

Solution

The NAA bridge delegates token acquisition to the host frame, where the MSAL PublicClientApplication runs in the top-level browsing context without partitioning issues.

Extensions opt in by calling SDK.enableNestedAppAuth() after SDK.init(). This sets up window.nestedAppAuthBridge, which MSAL's createNestablePublicClientApplication discovers automatically.

How it works

  1. Extension calls SDK.enableNestedAppAuth() after SDK.init()
  2. The SDK obtains the DevOps.NestedAppAuth remote proxy from the host via XDM
  3. It sets window.nestedAppAuthBridge with addEventListener/postMessage (the interface MSAL expects)
  4. When MSAL calls postMessage(requestJson), the shim forwards it to the host via XDM
  5. The host response is dispatched to registered message listeners

If the host doesn't support NAA, enableNestedAppAuth() throws with a clear error message.

Files changed

  • src/NestedAppAuthBridge.ts (new) -- Bridge shim implementation
  • src/SDK.ts -- Exports enableNestedAppAuth() and disableNestedAppAuth() functions

Extension usage

import * as SDK from 'azure-devops-extension-sdk';
import { createNestablePublicClientApplication, InteractionRequiredAuthError } from '@azure/msal-browser';

await SDK.init();
await SDK.enableNestedAppAuth();

const pca = await createNestablePublicClientApplication({
    auth: { clientId: 'your-entra-client-id' }
});

try {
    const result = await pca.acquireTokenSilent({
        scopes: ['https://graph.microsoft.com/.default'],
    });
} catch (err) {
    if (err instanceof InteractionRequiredAuthError) {
        const result = await pca.acquireTokenPopup({
            scopes: ['https://graph.microsoft.com/.default'],
        });
    } else {
        throw err;
    }
}

Prerequisites

  • Register a SPA app in the Azure Portal with redirect URI: https://dev.azure.com/_public/_MsalPopup
  • Configure API permissions for the scopes your extension needs
  • No changes to the extension manifest are required

Mihaela Korte and others added 3 commits May 9, 2026 18:14
Implements the client-side NAA bridge shim that enables Azure DevOps
extensions to use MSAL v5's createNestablePublicClientApplication for
authentication. Extensions running in cross-origin iframes cannot use
MSAL v5's BroadcastChannel-based redirect flow due to browser storage
partitioning. The NAA bridge solves this by delegating token acquisition
to the host frame.

The shim translates between MSAL's event-based postMessage/addEventListener
API (window.nestedAppAuthBridge) and the host frame's XDM-based
processRequest method (DevOps.NestedAppAuth).

The bridge is initialized automatically after SDK.init() completes. If the
host does not support NAA (feature flag off), initialization is silently
skipped.

Companion change in the ADO monorepo: PR #920218 (host-side bridge handler)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of auto-initializing the bridge for every extension,
expose SDK.enableNestedAppAuth() that extensions call explicitly.
This avoids unnecessary XDM traffic for extensions that don't
need NAA, and gives extensions a clear error if the host doesn't
support it (missing entraClientId or feature flag off).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…docs

- Replace getRemoteObjectProxy with invokeRemoteMethod for XDM calls
  (host-side serializer does not proxy functions across the boundary)
- Add Ping-based availability check during bridge initialization
- Update error message (removed entraClientId reference)
- Fix indentation in bridge object literal
- Add Nested App Authentication section to README with usage guide

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mihaelams1 mihaelams1 marked this pull request as ready for review May 14, 2026 06:01
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/NestedAppAuthBridge.ts Outdated
Comment thread src/SDK.ts
- Improve error message to 'not available in this Azure DevOps environment yet'
  (Claire's feedback)
- Add disableNestedAppAuth() + teardownNestedAppAuthBridge() for cleanup
  (Claire's feedback)
- Fix 'ADO' abbreviation in README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mihaelams1 mihaelams1 force-pushed the users/mkorte/naa-bridge-for-extensions branch from bc6ddfc to 4428375 Compare May 14, 2026 17:41
After disableNestedAppAuth(), replace window.nestedAppAuthBridge with
a stub whose postMessage/addEventListener throw a clear error instead
of deleting the property. This prevents MSAL from silently falling
back to the broken popup redirect flow when the bridge is disabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mihaelams1 mihaelams1 force-pushed the users/mkorte/naa-bridge-for-extensions branch from 4428375 to 9acf17e Compare May 14, 2026 17:45
Mihaela Korte and others added 2 commits May 14, 2026 11:15
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The poisoned bridge was replacing the entire window.nestedAppAuthBridge
object, which orphaned MSAL's listener that was registered via
addEventListener on the original object. This caused acquireTokenPopup
to hang after disableNestedAppAuth() — the poisoned response was
delivered to an empty listeners array.

Fix: store listeners in a _naaListeners property on the bridge object
during initialization, and have teardown mutate only the postMessage
function in-place. This way existing MSAL listeners continue to
receive responses (now poisoned BridgeDisabled errors).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@clairebaire clairebaire self-requested a review May 14, 2026 18:45
Mihaela Korte and others added 2 commits May 26, 2026 13:28
Document that acquireTokenSilent will fail with consent_required on
first use, and that the InteractionRequiredAuthError fallback handles
this automatically via popup. Also document explicit consent prompt
and admin consent for elevated permissions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mihaelams1 mihaelams1 merged commit 6ad1367 into master Jun 9, 2026
3 checks passed
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