Fabric SSO
Sign users in with their Microsoft Entra identity through the Fabric portal — enable it in rayfin.yml, install the provider package, and call ensureSignedInWithFabric.
Fabric SSO lets users sign in to a deployed Rayfin app with the Microsoft Entra identity they already used to open the Fabric portal. There's no separate login form — the user authenticates once in Fabric and the app inherits that session.
Note
Fabric SSO requires a backend deployed to Fabric — not a frontend deployed to Fabric.
The popup flow signs users in from any origin listed in allowedRedirectUris, including
a local Vite dev server at http://localhost:5173. Only the embedded flow needs the app
to be running inside the Fabric portal. See
Local frontend development.
The SDK supports two flows:
- Popup flow — the app opens the Fabric portal in a new browser tab, the user authenticates, and the tab closes automatically.
- Embedded flow — the app runs inside a Fabric iframe and inherits the session via
postMessage, with no popup, redirect, or user interaction.
Both flows use PKCE (S256), validate the postMessage origin, and tie the exchange to a
state nonce.
How it works
Popup flow
- The app opens the Fabric portal in a new browser tab and registers a
postMessagelistener. - The user authenticates through Entra ID inside the Fabric portal.
- The Fabric extension sends the handoff code back to the app via
window.top.opener.postMessage(). - The SDK exchanges the handoff code for Rayfin session tokens and creates a session.
- The Fabric tab closes automatically.
No callback page or redirect is needed.
Embedded flow (Fabric iframe)
- The Fabric shell loads the app inside an iframe with
?fabricEmbedded=truein the URL. - On startup the app detects embedded mode and calls
initEmbeddedAuth(). - The SDK generates PKCE parameters in memory and sends
auth.requestHandoffto the parent frame viapostMessage. - The Fabric extension host responds with a handoff code.
- The SDK exchanges the handoff code for Rayfin session tokens and creates a session.
No popup, redirect, or user click is needed.
Local frontend development
The popup flow works from localhost. What Fabric SSO needs is a deployed backend —
the frontend calling it can be served by Vite:
-
Deploy the backend, skipping the static frontend:
npx rayfin up --exclude-services staticHosting -
Confirm your dev origin is in
allowedRedirectUris—http://localhost:5173is the default. -
Start Vite and sign in from the button that calls
ensureSignedInWithFabric():npm run dev
The popup opens the Fabric portal, the user authenticates there, and the handoff code is
posted back to returnOrigin — your localhost origin. Sessions, claims, and row-level
security behave exactly as they do in the deployed app.
The embedded flow is the exception. It needs the Fabric shell to load your app in an
iframe, so it only runs once npx rayfin up has deployed the static frontend and you open
the app from the Fabric portal. ensureSignedInWithFabric() falls back to the popup when
the app isn't embedded, so the same call covers both cases.
Enable Fabric SSO
Add the fabric block and your app's origin to rayfin/rayfin.yml:
services:
auth:
enabled: true
allowedRedirectUris:
- http://localhost:5173
fabric:
enabled: trueallowedRedirectUris must include your app's bare origin (for example
http://localhost:5173) — the popup flow uses it as the postMessage target origin for the
handoff code. See Redirect URIs for the full picture,
including what rayfin up appends automatically on deploy.
After changing rayfin.yml, redeploy so the setting takes effect:
npx rayfin upInstall the provider package
Fabric auth ships as a separate companion package:
npm install @microsoft/rayfin-auth-provider-fabricClient-side usage
Popup flow: sign in from a button click
Call ensureSignedInWithFabric from a user-gesture handler, such as a button's onClick.
The function's last step calls window.open(), so it needs a synchronous user gesture to
avoid popup blockers.
import { RayfinClient } from '@microsoft/rayfin-client';
import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric';
const client = new RayfinClient({
baseUrl: import.meta.env.VITE_RAYFIN_API_URL,
publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,
});
async function handleSignIn() {
const session = await ensureSignedInWithFabric(client.auth, {
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,
});
console.log('Signed in:', session.user);
}Embedded flow: automatic sign-in on startup
Call initEmbeddedAuth() once at app startup — for example in a React useEffect or an
initialization routine. It's safe to call on every page load: it returns null immediately
when the app isn't running in embedded mode.
import { RayfinClient } from '@microsoft/rayfin-client';
import { initEmbeddedAuth } from '@microsoft/rayfin-auth-provider-fabric';
const client = new RayfinClient({
baseUrl: import.meta.env.VITE_RAYFIN_API_URL,
publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,
});
const fabricOptions = {
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,
};
// Safe to call on every page load — a no-op when not embedded.
const session = await initEmbeddedAuth(client.auth, fabricOptions);
if (session) {
console.log('Embedded session established:', session.user);
}Warning
Import @microsoft/rayfin-auth-provider-fabric statically in your app's entry module —
not only via a dynamic import(). The package captures the ?fabricEmbedded=true URL flag
into sessionStorage as a side effect of module load, and that has to happen on the
initial page load, before any client-side navigation strips the query string (for example a
post-logout redirect to /login). An app that resumes from a stored refresh token never
takes the embedded-auth path on first load, so a dynamic-only import would miss the URL
flag and fall back to the popup on the next sign-in.
Supporting both flows
Most apps should support both the popup flow (standalone browser) and the embedded flow
(iframe). ensureSignedInWithFabric() handles this automatically — it tries embedded auth
first, then falls back to the popup:
- Return the existing session if already authenticated.
- Attempt a silent refresh via the refresh token.
- If embedded mode is detected, use the
postMessagehandoff (no popup). - Otherwise, open the Fabric portal in a new tab and wait for the handoff.
For page-load initialization with no user gesture available, use initEmbeddedAuth()
instead — it skips step 4 and returns null when no embedded session is available.
React hook example
import { useState, useCallback } from 'react';
import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric';
import { client } from '../services/rayfinClient';
const fabricOptions = {
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,
};
export function useFabricAuth() {
const [session, setSession] = useState(client.auth.getSession());
const signIn = useCallback(async () => {
const result = await ensureSignedInWithFabric(client.auth, fabricOptions);
setSession(result);
return result;
}, []);
return { session, signIn, isAuthenticated: session?.isAuthenticated ?? false };
}See React integration for a fuller pattern that also covers local development and route guarding.
API reference
ensureSignedInWithFabric(auth, options)
function ensureSignedInWithFabric(
auth: Auth,
options: FabricAuthOptions
): Promise<OpaqueSession>;The primary entry point. Implements the four-step waterfall described above — the first step that succeeds short-circuits the rest. 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 null when not embedded. Never opens a popup or new tab — safe for page-load use.
initiateFabricLogin(auth, options)
function initiateFabricLogin(auth: Auth, options: FabricAuthOptions): Promise<void>;Low-level function that opens the Fabric portal and listens for the postMessage handoff,
with no session or refresh-token pre-checks. Called internally by
ensureSignedInWithFabric in step 4. Most apps should use ensureSignedInWithFabric
instead.
isEmbeddedMode(options)
function isEmbeddedMode(options: FabricAuthOptions): boolean;Reports whether the SDK considers the current page to be running in embedded mode, without
starting a sign-in flow. Detection is true when options.fabricEmbedded is true, the URL
contains ?fabricEmbedded=true, or a previous call already stored that flag in
sessionStorage.
FabricAuthOptions
| Property | Type | Description |
|---|---|---|
workspaceId | string | The Fabric workspace ID. |
projectId | string | The Rayfin item ID (the AppBackend artifact ID). |
fabricPortalUrl | string | The Fabric portal base URL, e.g. https://app.fabric.microsoft.com. Existing path and query parameters are preserved. |
returnOrigin | string | Your app's bare origin, e.g. window.location.origin. Used as the postMessage target origin. |
fabricEmbedded | boolean (optional) | Force embedded mode. The SDK also auto-detects it from ?fabricEmbedded=true in the URL. |
callbackUrl | string (optional, deprecated) | Full callback URL for legacy Fabric portals that redirect instead of using postMessage. Defaults to ${returnOrigin}/auth/callback. Only needed for backward compatibility — will be removed once the postMessage rollout is complete. |
Legacy callback bridge
Newer Fabric portals hand off the sign-in code via postMessage. Older portals instead
redirect the popup to a callback page in your app. For that legacy path, call
bridgeFabricCallback() as early as possible on the callback page — if the URL contains
Fabric handoff parameters, it forwards them to the opener window and closes the popup.
import { bridgeFabricCallback } from '@microsoft/rayfin-auth-provider-fabric';
const bridged = bridgeFabricCallback();
if (!bridged) {
console.log('No Fabric handoff detected');
}This function is deprecated — it exists only for backward compatibility with Fabric portals
that haven't adopted postMessage yet, and will be removed once that rollout completes.
Security
- PKCE S256 — every flow generates a cryptographic code verifier and challenge to prevent authorization code interception.
- State nonce — a random nonce ties the
postMessageresponse to the originating flow, preventing CSRF. - In-closure code verifier — the PKCE code verifier is held in memory only and never
persisted to
localStorage. - Origin validation — the SDK validates
event.originon incoming messages againstfabricPortalUrl; the Fabric extension uses an explicittargetOrigin(never"*") when sending the handoff code. - Flow timeout — the flow times out after 5 minutes if no
postMessageis received. - Session isolation — in embedded mode, the session lives in the iframe's own
localStorage, isolated from the parent frame by the browser's same-origin policy.
Environment variables
Fabric auth needs three values at runtime to build FabricAuthOptions. npx rayfin up
writes the underlying RAYFIN_PUBLIC_* values to rayfin/.env, and rayfin env --framework vite (run automatically by the scaffolded predev / prebuild hooks) maps them to
Vite-compatible names in .env.local.
Source variable (rayfin/.env) | Vite variable (.env.local) | Maps to | Example |
|---|---|---|---|
RAYFIN_PUBLIC_ITEM_ID | VITE_FABRIC_ITEM_ID | projectId | 21b98705-08d5-448c-ab32-d88a3d00af41 |
RAYFIN_PUBLIC_WORKSPACE_ID | VITE_FABRIC_WORKSPACE_ID | workspaceId | b80c0e39-468a-4742-8f0a-458dc6b1c918 |
RAYFIN_PUBLIC_PORTAL_URL | VITE_FABRIC_PORTAL_URL | fabricPortalUrl | https://app.fabric.microsoft.com/ |
For local development, add these to rayfin/.env directly:
RAYFIN_PUBLIC_ITEM_ID=<your-rayfin-item-id>
RAYFIN_PUBLIC_WORKSPACE_ID=<your-fabric-workspace-id>
RAYFIN_PUBLIC_PORTAL_URL=https://app.fabric.microsoft.com/Deployment values
After npx rayfin up, the CLI records deployment metadata in rayfin/.deployments.json and
merges the corresponding RAYFIN_PUBLIC_* variables into rayfin/.env. Run rayfin env --framework vite (or npm run dev, which triggers it via the scaffolded predev hook) to
regenerate .env.local with the Vite-compatible names — use VITE_FABRIC_ITEM_ID as
projectId and VITE_FABRIC_WORKSPACE_ID as workspaceId in your FabricAuthOptions.
Troubleshooting
- Popup blocked — call
ensureSignedInWithFabricfrom a synchronous user-gesture handler, such as a button'sonClick. Calling it on page load, or after anawaitand before the user clicks, triggers popup blockers. - Session not persisting — confirm
RayfinClientis configured with the correctbaseUrlandpublishableKey. - Timeout after 5 minutes — the handoff code was never received. Check that
returnOriginmatches your app's actual origin and that the Fabric extension is sending to the correct origin. - Origin mismatch —
fabricPortalUrlmust match the origin of the Fabric portal tab. Verify you're using the correct URL for your environment (production, PPE, or dev). initEmbeddedAuthreturnsnull— ensure the URL contains?fabricEmbedded=true, or setfabricEmbedded: trueexplicitly in the options.- Embedded handoff timeout — the parent frame never responded. Verify
returnOriginmatches the iframe's actual origin. - State mismatch error — the response state didn't match the request state. This can indicate a replay attack or a stale response from a previous flow.
Add Fabric SSO sign-in to my Rayfin app.
Enable services.auth.fabric in rayfin/rayfin.yml (keep allowedRedirectUris scoped to my
app's own origin), install @microsoft/rayfin-auth-provider-fabric, and wire up
ensureSignedInWithFabric() from a button click handler for the popup flow, plus
initEmbeddedAuth() in a startup effect for the embedded/iframe flow.
Import @microsoft/rayfin-auth-provider-fabric statically at the app's entry point, not via a
dynamic import — it needs to capture the ?fabricEmbedded=true URL flag on first load.
Use onSessionChange to react to session changes — onAuthStateChange does not exist. Gate UI
on isAuthenticated or the presence of a user property rather than introspecting the session.
Make sure my dev origin is in services.auth.allowedRedirectUris so I can test the popup flow
from my local Vite server — it only needs the backend deployed to Fabric, not the frontend.
Tell me the exact commands to deploy the backend and start the dev server.