Skip to content
Udibo
On this page
View Markdown

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:

ValueUse
Issuer URLYour tenant's issuer, https://{tenant-id}.udibo.com; not your app's origin
Client ID and secretA confidential client registration for your backend
Callback URLAn exact registered URL, such as https://app.example.com/auth/callback
Allowed scopesIdentity claims and API permissions your application may request
Introspection or JWKS configurationToken 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.

mermaid
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 BFF

Mount 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.

TypeScript
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

TypeScript
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 401 and insufficient scopes receive 403.

  • 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 runSessionStoreContractTests from @udibo/oauth2/hono/bff/testing against your store. Stateful updates must reject expired, missing, or revoked sessions.

  • Confirm callbacks work when login and callback requests reach different application instances.

Troubleshooting

SymptomCheck
Callback rejected before sign-inExact callback registration: scheme, host, port, path, and query
invalid_clientBackend client ID/secret and whether this is the correct registration
Unknown state or missing login cookieShared pending-login storage, cookie attributes, and whether callback uses the same browser
Credentialed BFF request gets 403CSRF header and same-origin request configuration
Refresh gives invalid_grantExpiry or revocation; start a new login instead of retrying the credential indefinitely
API rejects a tokenIssuer, 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.