# 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](https://www.udibo.com/) for how to get one. To develop without an account, use the [local identity provider](https://www.udibo.com/docs/oauth2/guides/run-a-local-identity-provider) or the [external-auth example](https://www.udibo.com/docs/oauth2/examples/hono/app-with-external-auth). ## What you need Register a confidential application in your tenant's dashboard, as described in [Connect your first application](https://www.udibo.com/docs/identity/get-started). 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. ```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](https://www.udibo.com/docs/oauth2/guides/protect-an-api). Both are declared here so the example makes its application-owned dependencies explicit. ```ts 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; 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](https://www.udibo.com/docs/oauth2/known-limitations#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](https://www.udibo.com/docs/oauth2/guides/protect-an-api#when-the-api-is-a-separate-service). ## Connect the browser ```ts 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](https://www.udibo.com/docs/oauth2/templates/react-router) 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 | 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](https://www.udibo.com/docs/oauth2/guides/deploy-across-environments) and [application deployment](https://www.udibo.com/docs/oauth2/guides/production-deployment) for the remaining app-side setup. Tenant configuration — sign-in methods, organizations, permissions, and webhooks — is covered in the [Udibo Identity guides](https://www.udibo.com/docs/identity).