Skip to content

Tutorial: Proxy Mode

Build the same Team Notes app from the React tutorial, but using proxy mode instead of authz mode.

Key difference: In proxy mode, Sentinel handles the entire OAuth flow. The frontend redirects to Sentinel, which authenticates with the IdP, issues a single JWT, and redirects back. No dual tokens. No IdP configuration on the client.

Use proxy mode when you want Sentinel to own the login flow end-to-end. Use authz mode (recommended) when you want the frontend to authenticate with the IdP directly and keep Sentinel as a pure authorization service.

Prerequisites

Same as the React tutorial, except:

  • No Google Client ID needed on the frontend (Sentinel handles IdP config)
  • Client app redirect URI: http://localhost:5173/auth/callback

Backend Differences

Config

The Sentinel class takes mode="proxy" and does not need idp_jwks_url.

# config.py
from sentinel_auth import Sentinel

sentinel = Sentinel(
    base_url="http://localhost:9003",
    service_name="team-notes",
    service_key="sk_your_key_here",
    mode="proxy",
    actions=[
        {"action": "notes:export", "description": "Export notes as JSON"},
    ],
)

Middleware

sentinel.protect(app) adds JWTAuthMiddleware (not AuthzMiddleware). It validates a single Authorization: Bearer <sentinel_jwt> token per request.

# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from config import sentinel

app = FastAPI(title="Team Notes", lifespan=sentinel.lifespan)
sentinel.protect(app, exclude_paths=["/health", "/docs", "/openapi.json"])

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Routes

The routes are identical to the authz-mode tutorial. get_current_user, require_role, sentinel.require_action, and sentinel.permissions.can all work the same way. The middleware populates request.state.user regardless of mode.

One difference: request.state.token contains the Sentinel JWT (single token) instead of the authz token. The get_token helper works identically.

# Same routes as react.md Steps 3 -- no changes needed
from sentinel_auth.dependencies import get_current_user, get_workspace_id, require_role

Frontend Differences

Auth Client

Use SentinelAuth instead of SentinelAuthz. No IdP configuration needed -- Sentinel handles it.

// src/api/client.ts
import { SentinelAuth } from "@sentinel-auth/js";

const SENTINEL_URL = import.meta.env.VITE_SENTINEL_URL || "http://localhost:9003";
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:9200";

export const sentinelClient = new SentinelAuth({
  sentinelUrl: SENTINEL_URL,
});

export async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
  return sentinelClient.fetchJson<T>(`${BACKEND_URL}${path}`, options);
}

sentinelClient.fetchJson() attaches a single Authorization: Bearer <token> header (no X-Authz-Token).

Provider + App Shell

Use SentinelAuthProvider, AuthGuard, and AuthCallback instead of the authz variants.

// src/main.tsx
import { SentinelAuthProvider } from "@sentinel-auth/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { App } from "./App";
import { sentinelClient } from "./api/client";

const queryClient = new QueryClient();

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <SentinelAuthProvider client={sentinelClient}>
      <QueryClientProvider client={queryClient}>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </QueryClientProvider>
    </SentinelAuthProvider>
  </StrictMode>,
);
// src/App.tsx
import { AuthGuard } from "@sentinel-auth/react";
import { Route, Routes } from "react-router-dom";
import { AuthCallback } from "./pages/AuthCallback";
import { Login } from "./pages/Login";
import { NoteList } from "./pages/NoteList";

export function App() {
  return (
    <Routes>
      <Route path="/auth/callback" element={<AuthCallback />} />
      <Route
        path="*"
        element={
          <AuthGuard fallback={<Login />} loading={<div>Loading...</div>}>
            <Routes>
              <Route path="/" element={<NoteList />} />
            </Routes>
          </AuthGuard>
        }
      />
    </Routes>
  );
}

Login

login("google") redirects to Sentinel, which redirects to Google. No IdP client ID needed on the frontend.

// src/pages/Login.tsx
import { useAuth } from "@sentinel-auth/react";

export function Login() {
  const { login } = useAuth();
  return <button onClick={() => login("google")}>Sign in with Google</button>;
}

Callback

// src/pages/AuthCallback.tsx
import { AuthCallback as SentinelCallback } from "@sentinel-auth/react";
import { useNavigate } from "react-router-dom";

export function AuthCallback() {
  const navigate = useNavigate();
  return (
    <SentinelCallback
      onSuccess={() => navigate("/notes", { replace: true })}
      workspaceSelector={({ workspaces, onSelect, isLoading }) => (
        <div>
          <h2>Select Workspace</h2>
          {workspaces.map((ws) => (
            <button key={ws.id} onClick={() => onSelect(ws.id)} disabled={isLoading}>
              {ws.name} ({ws.role})
            </button>
          ))}
        </div>
      )}
    />
  );
}

Pages

Use useUser instead of useAuthzUser. Everything else is the same.

// src/pages/NoteList.tsx
import { useUser } from "@sentinel-auth/react";
import { useQuery } from "@tanstack/react-query";
import { apiFetch } from "../api/client";

export function NoteList() {
  const user = useUser();
  const canCreate = ["editor", "admin", "owner"].includes(user.workspaceRole);

  const { data: notes = [] } = useQuery({
    queryKey: ["notes"],
    queryFn: () => apiFetch<any[]>("/notes"),
  });

  return (
    <div>
      <h1>Notes</h1>
      {canCreate && <button>New Note</button>}
      <ul>
        {notes.map((note) => (
          <li key={note.id}>{note.title}</li>
        ))}
      </ul>
    </div>
  );
}

Side-by-Side Comparison

Aspect AuthZ Mode Proxy Mode
Who handles IdP login Frontend (direct) Sentinel (redirect)
Tokens per request 2 (IdP + authz) 1 (Sentinel JWT)
Frontend IdP config Required (client ID, JWKS) Not needed
JS client class SentinelAuthz SentinelAuth
React provider AuthzProvider SentinelAuthProvider
Route guard AuthzGuard AuthGuard
User hook useAuthzUser useUser
Auth hook useAuthz useAuth
Callback component AuthzCallback AuthCallback
Backend middleware AuthzMiddleware JWTAuthMiddleware
Backend routes Identical Identical
Permission/Role checks Identical Identical

The backend authorization logic (workspace roles, RBAC actions, entity ACLs) works identically in both modes. The difference is purely in how the user authenticates and how tokens are structured.