Rayfin

React integration

An auth context, a useAuth hook, and route guarding for React apps built on the Rayfin auth client with Fabric SSO.

Rayfin's auth client is framework-agnostic — getSession and onSessionChange work the same everywhere, and Fabric SSO's ensureSignedInWithFabric resolves to the same opaque session shape. This page wires them 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:

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

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

This is enough for a single-page app. Most real apps need one more thing: a shared provider so every component sees the same session without each one re-subscribing to onSessionChange itself. The rest of this page builds that.

An auth context and provider

src/services/rayfinClient.ts creates the shared RayfinClient and the FabricAuthOptions the provider needs:

src/services/rayfinClient.ts
import { RayfinClient } from '@microsoft/rayfin-client';

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

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

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

import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric';
import type { OpaqueSession } from '@microsoft/rayfin-auth';
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(() => {
    setSession(auth.getSession());
    setLoading(false);
    return auth.onSessionChange(setSession);
  }, []);

  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(null);
    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 the session state — every component that calls useAuth() sees the same session without re-subscribing to onSessionChange itself.

Guarding routes

A guard component reads useAuth() and redirects based on isAuthenticated, showing a loading state until the first session check resolves:

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

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

export default App;

Sign-in button and app entry point

ensureSignedInWithFabric's popup fallback calls window.open(), so signIn has to run from a synchronous user-gesture handler — wire it to a button, not an effect:

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

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

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

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

Sign-in completes from a local Vite dev server as long as your origin is in allowedRedirectUris and the backend is deployed — see Local frontend development. Everything else in this pattern — the provider, the guard, isAuthenticated — behaves the same regardless of which flow produced the session.

PromptAdd an AuthProvider, useAuth hook, and route guarding
In my Rayfin React app (Vite + react-router-dom), add authentication wiring: - An AuthProvider (React context) that wraps the app, calls client.auth.getSession() once on mount, subscribes with client.auth.onSessionChange(...), and exposes { session, loading, error, signIn, signOut, isAuthenticated } through a useAuth() hook. - signIn() should call ensureSignedInWithFabric() from @microsoft/rayfin-auth-provider-fabric, built from VITE_FABRIC_WORKSPACE_ID, VITE_FABRIC_ITEM_ID, and VITE_FABRIC_PORTAL_URL, and must be wired to a button's onClick so the popup isn't blocked. - An AuthGuard component used in the router that redirects unauthenticated users to /auth and authenticated users away from /auth, showing a loading state in between. Use onSessionChange, not onAuthStateChange — the latter does not exist on the Rayfin auth client. Do not introspect session fields beyond isAuthenticated, isAnonymous, user, role, and expiresAt. Show me the full set of files.
Something wrong on this page?Report an issueEdit this page

On this page