Skip to content

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 (no localAuthServerFetch).

  • Introspection-based resource server. IntrospectionTokenReader validates bearer tokens against the IDP's RFC 7662 endpoint and projects the response into the app's User/Client shape. 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's resolveUser reuses the same reader to look up the session user; a production app might call the IDP's OIDC /userinfo endpoint instead.

  • The React adapter. Identical to app-with-own-auth: routes/main.tsx wraps <OAuth2Provider>; routes/index.tsx uses useOAuth2() + <RequireAuth>.

Layout

Text
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):

Shell
deno task --cwd=../app-with-own-auth dev   # IDP on http://localhost:8004

In another, start this app:

Shell
deno task dev      # http://localhost:8005

Click 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).

Shell
deno task test
deno task check