app-with-external-auth (Juniper)
A SaaS app that delegates login to an external identity provider (Udibo, or
any RFC 6749 server) and protects its API by introspecting that IDP's tokens
— built with Juniper (SSR React on Deno) and
@udibo/oauth2.
It's the same SPA + BFF + React-adapter shape as app-with-own-auth, with the
embedded authorization server removed. Comparing the two oauth2/server.ts
files is the literal self-hosted → external migration path.
What it demonstrates
External IDP, still no tokens in the browser. The React SPA authenticates through the BFF's
/auth/*endpoints behind an HttpOnly cookie. The BFF's confidential client talks to the external IDP over real HTTP (nolocalAuthServerFetch).Introspection-based resource server.
IntrospectionTokenReadervalidates bearer tokens against the IDP's RFC 7662 endpoint and projects the response into the app'sUser/Clientshape.bff.protect(scope)gates each route — one inbound request plus one outbound introspection call apiece (cache the results or validate JWTs locally in production to drop the round-trip). With no shared in-process token service, the BFF'sresolveUserreuses the same reader to look up the session user; a production app might call the IDP's OIDC/userinfoendpoint instead.The React adapter. Identical to
app-with-own-auth:routes/main.tsxwraps<OAuth2Provider>;routes/index.tsxusesuseOAuth2()+<RequireAuth>.
Layout
oauth2/
server.ts # server-only: BFF + introspection resource server (no IDP)
browser-client.ts # browser-safe: the BffClient for the provider
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
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`There are no login / consent / oauth2 routes — those live on the external
IDP.
Run it (paired with the own-auth example as the IDP)
In one terminal, start the sibling example as the IDP (it registers this app's
http://localhost:8005/auth/callback redirect URI):
deno task --cwd=../app-with-own-auth dev # IDP on http://localhost:8004In another, start this app:
deno task dev # http://localhost:8005Click Sign in; you'll be sent to the IDP (8004) to log in (admin /
password or user / password), then bounced back here authenticated. As
user, GET /api/admin returns 403. Point IDP_BASE_URL and the credentials
in oauth2/server.ts at a real Udibo deployment for production; the example
sets cookie: { secure: false } for plain-HTTP local dev, so drop that override
(leave secure at its default) once you're on HTTPS.
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 in-process with no live IDP: it stubs tokenReader.getToken
(the scoped-override pattern the package recommends over a global fetch stub)
and injects a BFF session with createTestSession. CSRF is on by default, so a
credentialed cookie request must send the x-csrf header (the React adapter
adds it automatically in the SPA).
deno task test
deno task check
