Rayfin

React integration

Add a Rayfin AuthProvider, useAuth hook, route guard, Fabric SSO startup, and sign-in button to a React app.

Rayfin's auth client is framework-agnostic. getSession and onSessionChange work the same everywhere, and every supported sign-in flow resolves to the same opaque session shape. This page wires those APIs into React with a context, a hook, and route guarding.

A minimal useAuth hook

The smallest useful integration reads the current session once on mount, subscribes to changes, and exposes a signIn that wraps ensureSignedInWithFabric. Use this for a small single-page app that does not need a shared provider yet.

src/hooks/useAuth.ts
import { useCallback, useEffect, useState } from 'react';
import type { OpaqueSession } from '@microsoft/rayfin-auth';
import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric';
import { auth, fabricOptions } from '../services/rayfinClient';

export function useAuth() {
  const [session, setSession] = useState<OpaqueSession | null>(null);

  useEffect(() => {
    setSession(auth.getSession());
    return auth.onSessionChange(setSession);
  }, []);

  const signIn = useCallback(() => ensureSignedInWithFabric(auth, fabricOptions), []);

  return {
    session,
    isAuthenticated: session?.isAuthenticated ?? false,
    signIn,
    signOut: auth.signOut.bind(auth),
  };
}

Most apps need one more piece: a shared provider so every component sees the same session without re-subscribing. The rest of this page builds that provider.

Create the Rayfin client and Fabric options

src/services/rayfinClient.ts creates the shared RayfinClient and the FabricAuthOptions that the provider needs. rayfin dev writes the RAYFIN_PUBLIC_* values and maps them to Vite variables before starting the frontend.

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

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

export const auth = client.auth;

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,
};

Add AuthProvider and useAuth

The provider owns session state, exposes loading and error state during sign-in, and hands the same signIn and signOut functions to every consumer.

Call and await initEmbeddedAuth() before the initial loading state completes. This gives Fabric embedded auth and external embed host a chance to replace stale stored sessions before the UI trusts them. Changed in 1.36

src/hooks/AuthContext.tsx
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
  type ReactNode,
} from 'react';

import type { OpaqueSession } from '@microsoft/rayfin-auth';
import {
  ensureSignedInWithFabric,
  initEmbeddedAuth,
} from '@microsoft/rayfin-auth-provider-fabric';
import { auth, fabricOptions } from '../services/rayfinClient';

interface AuthContextValue {
  session: OpaqueSession | null;
  loading: boolean;
  error: string | null;
  signIn: () => Promise<OpaqueSession>;
  signOut: () => Promise<void>;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextValue | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [session, setSession] = useState<OpaqueSession | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let mounted = true;

    const unsubscribe = auth.onSessionChange((nextSession) => {
      if (mounted) {
        setSession(nextSession);
      }
    });

    async function initialize() {
      try {
        await initEmbeddedAuth(auth, fabricOptions);
        if (mounted) {
          setSession(auth.getSession());
        }
      } catch (err) {
        if (mounted) {
          setError(err instanceof Error ? err.message : 'Authentication failed');
          setSession(auth.getSession());
        }
      } finally {
        if (mounted) {
          setLoading(false);
        }
      }
    }

    void initialize();

    return () => {
      mounted = false;
      unsubscribe();
    };
  }, []);

  const signIn = useCallback(async () => {
    setError(null);
    setLoading(true);
    try {
      const result = await ensureSignedInWithFabric(auth, fabricOptions);
      setSession(result);
      return result;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Sign-in failed');
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  const signOut = useCallback(async () => {
    await auth.signOut();
    setSession(auth.getSession());
    setError(null);
  }, []);

  const value = useMemo<AuthContextValue>(
    () => ({
      session,
      loading,
      error,
      signIn,
      signOut,
      isAuthenticated: session?.isAuthenticated ?? false,
    }),
    [session, loading, error, signIn, signOut]
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth(): AuthContextValue {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

The provider talks to client.auth directly and owns session state. Every component that calls useAuth() sees the same session without subscribing to onSessionChange itself.

Guard routes

A guard component reads useAuth() and redirects based on isAuthenticated. It shows a loading state until the first session check and embedded startup flow resolve.

src/App.tsx
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { AuthPage } from './components/AuthPage';
import { useAuth } from './hooks/AuthContext';
import { HomePage } from './pages/HomePage';

function AuthGuard({
  children,
  requireAuth,
}: {
  children: React.ReactNode;
  requireAuth: boolean;
}) {
  const { isAuthenticated, loading } = useAuth();

  if (loading) return <div>Loading…</div>;
  if (requireAuth && !isAuthenticated) return <Navigate to="/auth" replace />;
  if (!requireAuth && isAuthenticated) return <Navigate to="/" replace />;

  return <>{children}</>;
}

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route
          path="/auth"
          element={
            <AuthGuard requireAuth={false}>
              <AuthPage />
            </AuthGuard>
          }
        />
        <Route
          path="/"
          element={
            <AuthGuard requireAuth={true}>
              <HomePage />
            </AuthGuard>
          }
        />
        <Route path="*" element={<Navigate to="/" replace />} />
      </Routes>
    </BrowserRouter>
  );
}

Add a sign-in button

ensureSignedInWithFabric() can open a popup. Call it directly from a synchronous click handler, not from a page-load effect.

src/components/AuthPage.tsx
import { useAuth } from '../hooks/AuthContext';

export function AuthPage() {
  const { signIn, loading, error } = useAuth();

  return (
    <main>
      <button onClick={() => void signIn()} disabled={loading}>
        Sign in with Microsoft Fabric
      </button>
      {error && <p role="alert">{error}</p>}
    </main>
  );
}

Wrap the app

Wrap the app in AuthProvider at the entry point.

src/main.tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import { AuthProvider } from './hooks/AuthContext';

createRoot(document.getElementById('root')!).render(
  <AuthProvider>
    <App />
  </AuthProvider>
);

Note

The popup flow works from a local Vite origin when that origin is in allowedRedirectUris. The embedded iframe flow runs only when the app is loaded by Fabric or by an external embed host.

PromptAdd an AuthProvider, useAuth hook, and route guarding
In my Rayfin React app, add authentication wiring. Create a shared RayfinClient and FabricAuthOptions from VITE_RAYFIN_API_URL, VITE_RAYFIN_PUBLISHABLE_KEY, VITE_FABRIC_WORKSPACE_ID, VITE_FABRIC_ITEM_ID, VITE_FABRIC_PORTAL_URL, and window.location.origin. Add an AuthProvider that subscribes with client.auth.onSessionChange(...), awaits initEmbeddedAuth(client.auth, fabricOptions) before clearing the initial loading state, and exposes { session, loading, error, signIn, signOut, isAuthenticated } through a useAuth() hook. signIn() must call ensureSignedInWithFabric() from a button click path. Add an AuthGuard that redirects unauthenticated users to /auth and authenticated users away from /auth. Do not use onAuthStateChange.
Something wrong on this page?Report an issueEdit this page

On this page