Use Udibo's identity service
Udibo handles sign-in and issues tokens for your application. Your backend completes the OAuth2 callback, maintains an application session, and protects application data.
You need a Udibo account to register your application; see udibo.com for how to get one. To develop without an account, use the local identity provider or the external-auth example.
What you need
Register a confidential application in your tenant's dashboard, as described in Connect your first application. The application's page shows its issuer and client ID:
| Value | Use |
|---|---|
| Issuer URL | Your tenant's issuer, https://{tenant-id}.udibo.com; not your app's origin |
| Client ID and secret | A confidential client registration for your backend |
| Callback URL | An exact registered URL, such as https://app.example.com/auth/callback |
| Allowed scopes | Identity claims and API permissions your application may request |
| Introspection or JWKS configuration | Token validation for your API; use the mode configured for the application |
Use separate registrations and secrets for development and production. Keep client credentials on the backend. The issuer and client ID are identifiers; a client secret is a credential.
Put the BFF on your app's origin
Mount the BFF at /auth. The browser talks to your own backend using its
session cookie. It never needs your client secret or refresh token.
How sign-in flows
The same flow applies when the issuer is your own authorization server.
sequenceDiagram
participant Browser
participant App as Your app / BFF
participant Issuer as Udibo or your app's authorization server
participant API as Your API
Browser->>App: Start sign-in
App-->>Browser: Redirect to authorization endpoint
Browser->>Issuer: Sign in
Issuer-->>Browser: Redirect back with code and state
Browser->>App: Complete callback
App->>Issuer: Exchange code with PKCE and client authentication
Issuer-->>App: Tokens
App-->>Browser: Session cookie
Browser->>App: Request application data
App->>API: Access token
API-->>Browser: Application data, through the BFFMount the BFF
The following is integration wiring. sessions is a persistent SessionStore
you implement for your app; resourceServer validates access tokens using the
API protection guide. Both are declared here so the example
makes its application-owned dependencies explicit.
import { Hono } from "hono";
import { DirectClient } from "@udibo/oauth2/client";
import {
EncryptedCookieAuthRequestStorage,
HonoBff,
type SessionStore,
} from "@udibo/oauth2/hono/bff";
import type { HonoResourceServer } from "@udibo/oauth2/hono/resource-server";
import { requestLogger } from "@udibo/oauth2/hono/log";
import type { ClientInterface } from "@udibo/oauth2/server";
declare const sessions: SessionStore;
declare const resourceServer: HonoResourceServer<ClientInterface, unknown>;
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing ${name}`);
return value;
}
const client = new DirectClient({
issuer: required("AUTH_ISSUER"),
clientId: required("AUTH_CLIENT_ID"),
clientSecret: required("AUTH_CLIENT_SECRET"),
redirectUri: required("AUTH_CALLBACK_URL"),
});
const bff = new HonoBff({
client,
sessionStore: sessions,
authRequestStorage: new EncryptedCookieAuthRequestStorage({
secret: required("AUTH_REQUEST_SECRET"),
}),
resourceServer,
scope: required("AUTH_SCOPES"),
defaultReturnTo: "/",
});
const app = new Hono();
app.use(requestLogger());
app.route("/auth", bff.routes());
app.use("/api/*", bff.protect());
app.get("/api/message", (c) => c.json({ message: "Authenticated" }));
export default app;AUTH_REQUEST_SECRET is an app-generated high-entropy secret shared by every
instance of this application. It protects the pending state and PKCE verifier
across the redirect. Store it in your deployment's secret manager. The example
keeps the BFF's HTTPS cookie and CSRF defaults enabled.
Keep EncryptedCookieAuthRequestStorage for any deployment with more than one
instance. MemoryAuthRequestStorageFactory keeps pending sign-ins in its
process's memory, so a callback served by another instance fails with an
unknown state; pass it only for a single process, tests and local
development. That in-memory store is capped at 1000 pending sign-ins and evicts the oldest first,
so anonymous starts cannot exhaust memory, but a burst of them can push out
real users' sign-ins. The cookie store keeps each browser's pending sign-ins in
that browser, so one client cannot evict another's. See
pending sign-in limits.
Behind a TLS-terminating proxy, the BFF sees http: request URLs. It builds
the SSO-logout post_logout_redirect_uri from AUTH_CALLBACK_URL's origin, so
that URI matches the https one you registered.
requestLogger() redacts every query value in its one line per request, including
callback code and state. Configure reverse proxies and tracing separately;
this middleware cannot redact logs emitted by other systems.
Use bff.protect("read") for a scope required throughout an API mount, or layer
resourceServer.requireScope(...) on individual routes after authentication.
Authentication alone does not check whether a user owns a specific record; that
authorization check belongs in your application.
For a separate API process, use bff.proxy(fixedApiUrl, options) and validate
tokens in that API. See
the proxy example.
Connect the browser
import { BffClient } from "@udibo/oauth2/client";
const auth = new BffClient();
const session = await auth.getSession();
if (!session.isAuthenticated) {
const { url } = await auth.login({ returnTo: "/" });
location.assign(url);
}
const response = await auth.fetch("/api/message");BffClient sends the BFF's CSRF header. A custom fetch client must send the
same header on credentialed requests. Do not disable CSRF to make a browser
request work.
React apps can use OAuth2Provider, useOAuth2, and RequireAuth from
@udibo/oauth2/react; the
React Router template shows that
wiring. A client-side guard controls rendering; your API still
requires server-side authorization.
The BFF's local logout ends the application session and attempts upstream token revocation. Ending the identity provider's SSO session is a separate operation; configure and verify that behavior for your application if you need it.
Test the integration
Before pointing it at production:
Complete sign-in and sign-out in a real browser with the registered callback.
Verify unauthenticated requests receive
401and insufficient scopes receive403.Expire an access token and check that a refresh preserves the session.
Revoke the session and check that an in-flight refresh cannot restore it.
Run
runSessionStoreContractTestsfrom@udibo/oauth2/hono/bff/testingagainst your store. Stateful updates must reject expired, missing, or revoked sessions.Confirm callbacks work when login and callback requests reach different application instances.
Troubleshooting
| Symptom | Check |
|---|---|
| Callback rejected before sign-in | Exact callback registration: scheme, host, port, path, and query |
invalid_client | Backend client ID/secret and whether this is the correct registration |
| Unknown state or missing login cookie | Shared pending-login storage, cookie attributes, and whether callback uses the same browser |
Credentialed BFF request gets 403 | CSRF header and same-origin request configuration |
Refresh gives invalid_grant | Expiry or revocation; start a new login instead of retrying the credential indefinitely |
| API rejects a token | Issuer, intended audience, token format, and required scopes |
See environment configuration and application deployment for the remaining app-side setup. Tenant configuration — sign-in methods, organizations, permissions, and webhooks — is covered in the Udibo Identity guides.

