app-with-own-auth (Juniper)
A SaaS app that owns its identity layer — it issues, protects, and serves
its own SPA — built with Juniper (SSR React
on Deno) and @udibo/oauth2.
This is the React/SSR counterpart to examples/hono/app-with-own-auth. It's the
first example to exercise the React adapter (OAuth2Provider, useOAuth2,
RequireAuth) against the BFF, so it doubles as the template a real Juniper
dashboard follows.
What it demonstrates
BFF (Backend-For-Frontend). The browser never holds a token. The React SPA authenticates through the BFF's
/auth/*endpoints behind an HttpOnly session cookie. Token exchange and refresh happen in-process vialocalAuthServerFetch— a browser → API call costs one inbound request and zero outbound.One process, three roles. An embedded authorization server (the IDP), the BFF, and a resource server all run together (
oauth2/server.ts). The resource server shares the auth server's token service, so bearer tokens validate in-process with no introspection round-trip.No consent handler needed. The SPA is this IDP's own first-party client, so the authorize route configures no
handleConsent— with no handler the framework grants without a prompt. Per-user limits still apply:ScopedTokenServicenarrows every issued token to the signed-in user'smaxScope, so the regularusergets 403 from theadmin-scoped endpoint whileadminsucceeds.The React adapter.
routes/main.tsxwraps the app in<OAuth2Provider>;routes/index.tsxusesuseOAuth2()for{ isAuthenticated, user, login, logout }to gate the protected section (so the landing page stays viewable when signed out), and wraps that section in<RequireAuth>as a belt-and-suspenders demo —<RequireAuth>is the tool for a standalone protected route, where it auto-redirects to login when signed out.
Layout
oauth2/
server.ts # server-only: authorization server + BFF + resource server
browser-client.ts # browser-safe: the BffClient for the provider
sessions.ts # the IDP login session (separate from the BFF token session)
html.ts # escape helper for the server-rendered login page
routes/
main.ts # root server middleware (logging)
main.tsx # root layout — <OAuth2Provider> wraps the app
index.tsx # the SPA: useOAuth2 + RequireAuth + scope-gated API calls
login.ts # IDP login form (server-rendered HTML)
logout.ts # IDP sign-out (clears the IDP session)
oauth2/main.ts # the authorization server's endpoints -> /oauth2/*
auth/main.ts # the BFF's browser endpoints -> /auth/*
api/main.ts # protected resources (bff.protect) -> /api/*
main.ts, main.tsx # auto-generated by `deno task build`Run it
deno task dev # http://localhost:8004 (or: deno task build && deno task serve)Click Sign in, log in as admin / password (or user / password), then
try the three API buttons. As user, GET /api/admin returns 403.
The demo client also registers a second redirect URI
(http://localhost:8005/auth/callback) so the sibling
examples/juniper/app-with-external-auth (port 8005) can pair with this server
as its external IDP for local development.
Eliminating the first-paint flash (SSR initialState)
This example uses the provider's on-mount session probe for simplicity, so the
first render is briefly unauthenticated. To render the authenticated shell on
the server instead, resolve the session in a server loader and pass it as
<OAuth2Provider initialState={...}>. The Udibo dashboard does this; the BFF's
sessionHandler() (used internally by /auth/session) is the building block.
OAuth2Provider re-syncs to a changed initialState (keyed on the
authenticated/user identity), so in an SSR+SPA app where the root loader re-runs
on client-side navigations — e.g. landing on / after a BFF login whose
callback completes server-side — the provider reflects the updated
server-resolved state without a full page reload. (With a BffClient there's no
client-side authenticated event, so this re-sync is what flips the UI to
signed-in post-login.)
Testing
The root middleware uses requestLogger() from @udibo/oauth2/hono/log. Both
request and response lines redact every query value, including callback codes
and state. Proxy and tracing logs need their own redaction configuration.
main.test.ts runs entirely in-process (server.request(...), no sockets, no
Docker). Protected-endpoint tests skip the browser redirect flow by minting a
token into the shared token service and injecting a BFF session with
createTestSession from @udibo/oauth2/hono/bff/testing.
deno task test
deno task check
