Rayfin

Fabric SSO

Sign users in with Microsoft Entra ID through Fabric SSO, using the popup flow for browser apps or embedded flow in the Fabric portal.

Fabric SSO signs users in with the Microsoft Entra identity they use for Fabric. Use it when your Rayfin browser app should let Fabric broker interactive sign-in. There is no separate login form for Rayfin Fabric apps.

Note

Fabric SSO requires a backend deployed to Fabric, not a frontend deployed to Fabric. The popup flow works from any origin in allowedRedirectUris, including http://localhost:5173. Only the embedded iframe flow requires the app itself to be loaded inside the Fabric portal.

The SDK supports two Fabric-brokered browser flows:

  • Popup flow — a standalone browser app opens the Fabric broker in a new tab, receives a handoff code, and exchanges it for a Rayfin session.
  • Embedded flow — a Rayfin app loaded inside the Fabric portal iframe requests a handoff from the parent frame with no popup, redirect, or user click.

For code that already has a delegated Entra token, use Direct Entra token sign-in. For a third-party portal that embeds a Rayfin app in an iframe, use External embed host. New in 1.36

How the popup flow works

  1. Your app calls ensureSignedInWithFabric() from a user gesture.
  2. The SDK opens a Fabric broker URL in a popup and registers postMessage listeners.
  3. The broker sends brokeredAuth.ready.
  4. The SDK replies with a PKCE S256 challenge and state nonce.
  5. The broker returns a handoff code.
  6. The SDK exchanges the handoff code for Rayfin tokens and installs an opaque session on client.auth.

No callback page or redirect is needed for the modern broker path. The popup closes automatically after the handoff completes.

The popup path preserves existing path and query parameters on fabricPortalUrl.

How the embedded flow works

  1. The Fabric shell loads the Rayfin app in an iframe with ?fabricEmbedded=true.
  2. Your app calls and awaits initEmbeddedAuth() once at startup.
  3. The SDK generates PKCE parameters in memory and posts auth.requestHandoff to the parent frame.
  4. The parent returns a handoff code.
  5. The SDK exchanges the handoff code for Rayfin tokens and installs the session.

In Rayfin 1.36, embedded startup first performs a short external-embed classification handshake. A third-party parent can acknowledge that handshake for external embed host. If no parent acknowledges, the normal Fabric embedded flow continues. Changed in 1.36

Await initEmbeddedAuth() before rendering authenticated content or trusting a stored session. Do not skip it when client.auth.getSession() already reports an authenticated session; in embedded scenarios the SDK may need to discard a stale session from a different user.

Local frontend development

The popup flow works from localhost. Fabric SSO needs a deployed Fabric backend, but the frontend can be served by Vite from an allowed origin. Only the embedded iframe flow needs the app itself to be loaded inside the Fabric portal.

  1. Sign in to the Rayfin CLI on your development machine:

    npx rayfin login
  2. Keep your local frontend origin in allowedRedirectUris. http://localhost:5173 is the default Vite origin:

    rayfin/rayfin.yml
    services:
      auth:
        enabled: true
        allowedRedirectUris:
          - http://localhost:5173
        fabric:
          enabled: true
  3. Start the development loop:

    npm run dev

    In scaffolded projects, npm run dev runs rayfin dev. That command provisions or reuses the Fabric backend, applies runtime settings, writes RAYFIN_PUBLIC_* values to rayfin/.env, maps them to framework variables such as VITE_FABRIC_ITEM_ID, and starts Vite.

  4. Sign in from the button that calls ensureSignedInWithFabric(). The popup opens the Fabric broker, the user authenticates there, and the handoff returns to returnOrigin — your local Vite origin.

The embedded flow is different. It runs only after npx rayfin up deploys the static frontend and you open the app from the Fabric portal. ensureSignedInWithFabric() falls back to the popup when the app is not embedded, so the same sign-in button still covers standalone local development.

Scripts can still deploy the backend without publishing static hosting:

npx rayfin up --exclude-services staticHosting

Use rayfin dev for the normal development loop. Use rayfin up --exclude-services staticHosting for automation that needs to update the backend while another process serves the frontend.

Local automatic sign-in

Some 1.36 templates use the local development adapter to seed auth during rayfin dev. When services.staticHosting.assetAccess is protected, the Vite adapter exposes a loopback-only /.rayfin/dev/session-token endpoint for the local dev server. New in 1.36

That endpoint uses the developer's rayfin login session to obtain a delegated Entra token. It exchanges that token for a Rayfin token response and app code installs it with signInWithBrokeredToken(). Public static hosting can opt into the same behavior with rayfinLocalDev({ autoLogin: true }). The exchange requires services.auth.fabric.externalEntraExchange: true, which the Universal App template sets.

The 1.36 universal app template wires this in a development-only code path: it dynamically imports @microsoft/rayfin-local-dev, checks isRayfinLocalAutoLoginEnabled(), calls fetchRayfinLocalSessionToken(), then passes the result to signInWithBrokeredToken(client.auth, sessionToken). See Develop locally for the full local dev routing and automatic sign-in behavior.

Use the helper functions from @microsoft/rayfin-local-dev only in development-only code paths:

src/auth/localAutoLogin.ts
import { signInWithBrokeredToken } from '@microsoft/rayfin-auth-provider-fabric';
import { client } from '../services/rayfinClient';

export async function tryLocalAutoLogin() {
  if (!import.meta.env.DEV) return null;

  const localDev = await import('@microsoft/rayfin-local-dev');
  if (!localDev.isRayfinLocalAutoLoginEnabled()) return null;

  const sessionToken = await localDev.fetchRayfinLocalSessionToken();
  if (!sessionToken) return null;

  return signInWithBrokeredToken(client.auth, sessionToken);
}

If the endpoint is not active, fetchRayfinLocalSessionToken() returns undefined. If it is active but the token exchange fails, it throws an actionable error.

Enable Fabric SSO

Add the fabric block and every browser origin that will run the popup flow:

rayfin/rayfin.yml
services:
  auth:
    enabled: true
    allowedRedirectUris:
      - http://localhost:5173
    fabric:
      enabled: true

allowedRedirectUris uses bare origins such as http://localhost:5173 or https://app.example.com. The popup flow binds the handoff to returnOrigin, so the origin your app passes to the SDK must be allowed by the backend.

Apply the setting with the development loop or a deployment:

npm run dev
npx rayfin up

Install the provider package

Fabric auth ships as a separate companion package:

npm install @microsoft/rayfin-auth-provider-fabric

Keep Rayfin packages on the same version. rayfin up checks static-hosted apps for an auth SDK new enough for protected static hosting and may prompt you to upgrade Rayfin packages before uploading.

Build options from runtime config

Fabric auth needs Fabric coordinates at runtime to build FabricAuthOptions. Scaffolded projects receive these from rayfin dev or rayfin up.

Source variable (rayfin/.env)Vite variable (.env.local)Maps toExample
RAYFIN_PUBLIC_ITEM_IDVITE_FABRIC_ITEM_IDprojectId21b98705-08d5-448c-ab32-d88a3d00af41
RAYFIN_PUBLIC_WORKSPACE_IDVITE_FABRIC_WORKSPACE_IDworkspaceIdb80c0e39-468a-4742-8f0a-458dc6b1c918
RAYFIN_PUBLIC_PORTAL_URLVITE_FABRIC_PORTAL_URLfabricPortalUrlhttps://app.fabric.microsoft.com/
src/auth/fabricOptions.ts
import type { FabricAuthOptions } from '@microsoft/rayfin-auth-provider-fabric';

export const fabricOptions: FabricAuthOptions = {
  workspaceId: import.meta.env.VITE_FABRIC_WORKSPACE_ID,
  projectId: import.meta.env.VITE_FABRIC_ITEM_ID,
  fabricPortalUrl: import.meta.env.VITE_FABRIC_PORTAL_URL,
  returnOrigin: window.location.origin,
};

After rayfin dev or rayfin up, the CLI records deployment metadata in rayfin/.deployments.json and merges the corresponding RAYFIN_PUBLIC_* variables into rayfin/.env. The framework environment file is regenerated before the frontend starts.

Call ensureSignedInWithFabric() from a synchronous user-gesture handler, such as a button click. The function may call window.open(), so calling it on page load or after an unrelated await can trigger popup blockers.

src/auth/signIn.ts
import { RayfinClient } from '@microsoft/rayfin-client';
import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric';
import { fabricOptions } from './fabricOptions';

const client = new RayfinClient({
  baseUrl: import.meta.env.VITE_RAYFIN_API_URL,
  publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,
});

export async function signIn() {
  const session = await ensureSignedInWithFabric(client.auth, fabricOptions);
  console.log('Signed in:', session.user?.email);
  return session;
}

Embedded flow: automatic sign-in on startup

Call initEmbeddedAuth() once during app startup. It returns null when the page is not embedded and never opens a popup.

src/auth/bootstrap.ts
import { RayfinClient } from '@microsoft/rayfin-client';
import { initEmbeddedAuth } from '@microsoft/rayfin-auth-provider-fabric';
import { fabricOptions } from './fabricOptions';

const client = new RayfinClient({
  baseUrl: import.meta.env.VITE_RAYFIN_API_URL,
  publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,
});

export async function bootstrapAuth() {
  const embeddedSession = await initEmbeddedAuth(client.auth, fabricOptions);
  if (embeddedSession) {
    console.log('Embedded session established:', embeddedSession.user?.email);
  }
}

Warning

Import @microsoft/rayfin-auth-provider-fabric statically in your app entry module. The package captures the ?fabricEmbedded=true URL flag during module load, before client-side navigation can strip the query string.

Use both Fabric SSO flows

Most browser apps should support both environments:

  1. Call and await initEmbeddedAuth() at startup.
  2. Subscribe to client.auth.onSessionChange() for UI state.
  3. Wire ensureSignedInWithFabric() to a sign-in button for standalone browser use.

ensureSignedInWithFabric() runs this waterfall:

  1. Return a safe existing session.
  2. Try a refresh token.
  3. Use embedded postMessage handoff when Fabric embedded mode or the external-embed handshake applies.
  4. Open the Fabric broker popup.

For page-load initialization with no user gesture available, use initEmbeddedAuth() instead. It skips the popup step and returns null when no embedded session is available.

API reference

ensureSignedInWithFabric(auth, options)

function ensureSignedInWithFabric(
  auth: Auth,
  options: FabricAuthOptions
): Promise<OpaqueSession>;

The primary Fabric SSO entry point. Steps 1–3 are safe to call on page load. Step 4 opens a new browser tab and must run inside a user-gesture handler.

initEmbeddedAuth(auth, options)

function initEmbeddedAuth(
  auth: Auth,
  options: FabricAuthOptions
): Promise<OpaqueSession | null>;

Call once at app startup. Returns the authenticated session when running in embedded mode or external embed mode, or null when neither scenario applies.

initiateFabricLogin(auth, options)

function initiateFabricLogin(auth: Auth, options: FabricAuthOptions): Promise<void>;

Low-level popup function that opens the Fabric broker, delivers the PKCE challenge, waits for the handoff, exchanges the code, and installs the session. Most apps should use ensureSignedInWithFabric() instead.

signInWithBrokeredToken(auth, tokenResponse)

function signInWithBrokeredToken(
  auth: Auth,
  tokenResponse: TokenResponse
): Promise<OpaqueSession>;

Installs a Rayfin token response that a trusted caller already obtained from the brokered-token endpoint. The local development adapter uses this helper for automatic local sign-in. New in 1.36

FabricAuthOptions

PropertyTypeDescription
workspaceIdstringThe Fabric workspace ID.
projectIdstringThe Rayfin item ID or AppBackend artifact ID.
fabricPortalUrlstringThe Fabric portal base URL. Existing path and query parameters are preserved.
returnOriginstringYour app's bare origin, for example window.location.origin.
fabricEmbeddedboolean (optional)Force embedded mode. The SDK also auto-detects it from ?fabricEmbedded=true and stored embedded state.
callbackUrlstring (optional, deprecated)Full callback URL for legacy Fabric portals that redirect instead of using postMessage. Defaults to ${returnOrigin}/auth/callback.

Legacy callback bridge

Newer Fabric portals hand off the sign-in code via postMessage. Older portals can redirect the popup to a callback page in your app. For that legacy path, call bridgeFabricCallback() as early as possible on the callback page.

src/pages/AuthCallback.tsx
import { bridgeFabricCallback } from '@microsoft/rayfin-auth-provider-fabric';

const bridged = bridgeFabricCallback();
if (!bridged) {
  console.log('No Fabric handoff detected');
}

The function exists for backward compatibility with older Fabric portals.

Security details

  • Popup and embedded flows use PKCE S256 and state nonces.
  • The SDK holds the PKCE verifier in memory and does not persist it to localStorage.
  • Incoming popup handoff messages are correlated by state.
  • Popup origin checks are advisory because the broker extension frame can run at an origin different from fabricPortalUrl; state plus PKCE are the enforcement boundary.
  • Normal embedded auth.requestHandoff messages do not use fabricPortalUrl as targetOrigin because the parent frame is the Fabric extension iframe, not the top-level portal.
  • External embed handoff messages are pinned to the parent origin acknowledged during classification. New in 1.36
  • The popup flow times out after five minutes.

Troubleshooting

Symptom or errorMeaningFix
Popup blocked / TAB_BLOCKEDThe browser blocked the broker popup.Call ensureSignedInWithFabric() directly from a button click.
MISSING_WORKSPACE_ID, MISSING_PROJECT_ID, MISSING_RETURN_ORIGIN, MISSING_FABRIC_PORTAL_URLRequired FabricAuthOptions fields are absent.Build options from the VITE_FABRIC_* values produced by rayfin dev or rayfin up.
PKCE_CHALLENGE_FAILEDThe browser could not compute the PKCE challenge.Retry in a supported browser context.
CHALLENGE_DELIVERY_FAILEDThe SDK could not deliver the PKCE challenge to the broker frame.Retry sign-in and check browser popup or iframe restrictions.
Timeout after five minutes / FABRIC_AUTH_TIMEOUTThe popup handoff did not finish.Check returnOrigin, allowed redirect origins, and whether the broker tab was closed.
initEmbeddedAuth() returns nullThe app was not detected as embedded and no external embed host acknowledged.In Fabric, confirm the URL contains ?fabricEmbedded=true; in external embed, register the host before mounting the iframe.
NO_PARENT_WINDOWEmbedded auth was requested outside an iframe.ensureSignedInWithFabric() falls back to popup auth; remove stale fabricEmbedded flags if this recurs.
STATE_MISMATCHThe response state did not match the request state.Retry the flow from the start and investigate repeated failures as a security issue.
SESSION_NOT_ESTABLISHEDA handoff completed but no authenticated session was installed.Check backend auth configuration and SDK package versions.
Session not persistingRayfinClient is configured with the wrong backend or publishable key.Confirm baseUrl and publishableKey match the deployed or dev backend.
PromptAdd Fabric SSO sign-in
Add Fabric SSO sign-in to my Rayfin app. Enable services.auth.fabric in rayfin/rayfin.yml and keep allowedRedirectUris scoped to my app origins, including http://localhost:5173 for local frontend development. Install @microsoft/rayfin-auth-provider-fabric. At app startup, statically import the provider package and call initEmbeddedAuth(client.auth, fabricOptions). Await that call before rendering authenticated UI. Add a sign-in button that calls ensureSignedInWithFabric(client.auth, fabricOptions) directly from the click handler. Build FabricAuthOptions from VITE_FABRIC_WORKSPACE_ID, VITE_FABRIC_ITEM_ID, VITE_FABRIC_PORTAL_URL, and window.location.origin. Use client.auth.onSessionChange(...), not onAuthStateChange.
Something wrong on this page?Report an issueEdit this page

On this page