Skip to content

Repository files navigation

lovable-reference-implementation

A reference implementation for teams building vanilla React (Vite) SPAs that authenticate users against Okta via OIDC, gate every route behind that session, and call a GraphQL API via Apollo Client using the user's Okta access token. Built with:

  • Vite 5 + React 18 + TypeScript + Tailwind 3 + shadcn/ui
  • react-oidc-context (built on oidc-client-ts) for the OIDC client
  • Apollo Client 4 for the GraphQL client
  • React Router 6 for route-level auth gating
  • Vitest for tests

For the equivalent setup using Next.js + Auth.js + RSC, see the sibling repo nextjs-reference-implementation.


Architecture at a glance

                ┌──────────────┐
                │   Browser    │
                └──────┬───────┘
                       │
                       ▼
       ┌──────────────────────────────────┐
       │ <MatthewsAuthGraphqlProvider>    │  in src/App.tsx
       │   ├─ AuthProvider (Okta OIDC)    │  react-oidc-context
       │   └─ ApolloProvider              │  authLink + refreshLink + httpLink
       └──────────────┬───────────────────┘
                      │
                      ▼
       ┌──────────────────────────────────┐
       │ <RequireAuth>                    │  signinRedirect → Okta hosted login
       └──────────────┬───────────────────┘
                      │
                      ▼
        useQuery / useMutation / useSuspenseQuery
                      │
                      ▼
              Bearer = user.access_token
                      │
                      ▼
              VITE_GRAPHQL_API_URL

  All wired in: src/lib/matthews-auth-graphql/

Public surface

import {
  MatthewsAuthGraphqlProvider,
  RequireAuth,
  LoginCallback,
  useAuth,
} from "@/lib/matthews-auth-graphql";
Export Use
MatthewsAuthGraphqlProvider Wrap once in App.tsx above your routes
RequireAuth Route element that gates protected routes (auto-redirects to Okta's hosted sign-in)
LoginCallback Route element for redirect_uri (typically /callback)
useAuth Re-exported from react-oidc-context for session metadata

App builders import useQuery / useMutation / useSuspenseQuery directly from @apollo/client/react.


Getting started

For Humans

Copy and paste this prompt to your LLM agent (Claude Code, Cursor, AmpCode, etc.):

Read the conventions for this codebase before adding any routes, components, or auth/GraphQL code. The provider in src/lib/matthews-auth-graphql/ already handles Okta OIDC and Apollo wiring — don't duplicate it.

https://raw.githubusercontent.com/MatthewsREIS/lovable-reference-implementation/refs/heads/main/AGENTS.md

For LLM Agents

Fetch the conventions and follow them:

curl -s https://raw.githubusercontent.com/MatthewsREIS/lovable-reference-implementation/refs/heads/main/AGENTS.md

Local setup

# Install dependencies
npm install

# Copy env template and fill in the Okta values
cp .env.example .env.local
# edit .env.local — see table below

# Start the dev server (port 3000 — must match VITE_OKTA_REDIRECT_URI)
npm run dev

Get the values from the Matthews portal — visit portal.matthews.com/console/builder/apps, choose your app, and copy the values from the configuration screen.

Variable Required Purpose
VITE_OKTA_ISSUER yes Base Okta domain, e.g. https://your-org.okta.comno /oauth2/default suffix. Uses the Org Authorization Server.
VITE_OKTA_CLIENT_ID yes Okta OIDC public client ID.
VITE_GRAPHQL_API_URL yes Browser-reachable GraphQL endpoint.
VITE_OKTA_REDIRECT_URI no Where Okta sends the user after consent. Defaults to ${origin}/api/auth/callback/okta — same path the nextjs sibling uses, so one Okta app config covers both repos. Override only if you need a different path.

If any required variable is missing, the app renders a helpful "Configuration required" screen at startup instead of a blank page.

If you don't own the Okta tenant, ask whoever administers it for VITE_OKTA_ISSUER and VITE_OKTA_CLIENT_ID, and have them add http://localhost:3000/api/auth/callback/okta (and your deployed callback URL) to the app's Sign-in redirect URIs. The nextjs sibling uses the same callback path, so one registration covers both repos — but they share the same default port (3000), so don't run both dev servers simultaneously.

Visit http://localhost:3000:

  1. <RequireAuth> finds no session and redirects to Okta's hosted sign-in page.
  2. After consent you land on /api/auth/callback/okta briefly, then on / with the demo page.
  3. Open DevTools → Network → VITE_GRAPHQL_API_URL request: the Authorization: Bearer … header carries the Okta access token.

Token refresh flow

  1. Initial sign-in. react-oidc-context runs the OIDC code-exchange via oidc-client-ts and stores access_token + refresh_token + expires_at in localStorage.
  2. Pre-emptive renewal. With automaticSilentRenew: true, oidc-client-ts schedules a refresh shortly before expiry and rotates the access token transparently.
  3. Reactive renewal on 401. If a request goes out with a just-expired token and the API returns 401, the Apollo refreshLink calls auth.signinSilent(), updates the bearer header, and retries the operation once. A second 401 bubbles up.

oidc-client-ts internally serializes silent-renew calls, so concurrent refreshes can't fight each other (the equivalent of the nextjs inflightRefreshes map).

If a refresh ultimately fails — e.g. the Okta refresh token has been revoked — useAuth().error surfaces the failure. The Sign-out button (or any code that calls auth.removeUser()) clears the local session and the user can re-authenticate.


Sign-out behavior

Sign-out is local-only by design: clicking Sign out clears this app's stored OIDC session and reloads /. The user's Okta session is untouched — they remain signed in to Okta and to every other Okta-backed application.

Caveat: with no intermediate "you're signed out" page, the reload re-runs <RequireAuth> which redirects to Okta. Because the SSO session is still alive, Okta silently re-issues a code and the user is signed back in immediately — the button looks like a no-op. This is the documented tradeoff of local-only logout without an intermediate landing page; matches common corporate-SSO patterns (Salesforce, Slack Enterprise, etc.) where "Sign out of App X" doesn't kill the SSO session.

For federated logout (kiosk/shared-machine scenarios where the SSO session must end too), redirect after removeUser() to ${VITE_OKTA_ISSUER}/oauth2/v1/logout?id_token_hint=…&post_logout_redirect_uri=…. You'll need to capture id_token from the OIDC user object and add the post-logout URL to Okta's Sign-out redirect URIs.


Scripts

Command Purpose
npm run dev Start the Vite dev server on :3000
npm run build Production build
npm run preview Preview the production build
npm run lint Run ESLint
npm test Run the Vitest unit suite
npm run test:watch Run Vitest in watch mode

Testing

npm test runs the Vitest unit suite. The suite covers the testable units of the package: env.ts, the Apollo link chain (token attach + 401-retry-once + give-up-on-second-401), and the <RequireAuth> route guard. The end-to-end auth flow against a real Okta tenant is out of scope.


Security notes

  • Tokens are stored in localStorage via oidc-client-ts's WebStorageStateStore. This is the standard SPA pattern; if your threat model excludes XSS, this is fine. If you have stricter requirements, swap WebStorageStateStore for an in-memory store (sessions won't survive reloads) or move auth behind a backend-for-frontend.
  • Access and refresh tokens never appear in the page HTML.
  • <RequireAuth> is a client-side gate. The authoritative check is the GraphQL API rejecting requests without a valid bearer — never trust the SPA gate alone for sensitive data.
  • Sign-out is local-only — see Sign-out behavior.

About

Reference Lovable implementation for builder apps demonstrating auth and API integration

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages