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:
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:
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:
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:
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:
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:
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.
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.