Use authentication in React
Use @udibo/oauth2/react to share session state and sign-in actions with your
components. For a browser app with a backend, connect it to BffClient so
tokens stay on the server. First mount your BFF using
the integration guide or the
app-owned authorization example.
Provide the client once
import { BffClient } from "@udibo/oauth2/client";
import { OAuth2Provider, RequireAuth, useOAuth2 } from "@udibo/oauth2/react";
const client = new BffClient();
function Account() {
const { user, logout } = useOAuth2();
return (
<section>
<p>Signed in as {String(user?.name ?? user?.sub ?? "user")}</p>
<button onClick={() => logout()}>Sign out</button>
</section>
);
}
export function App() {
return (
<OAuth2Provider client={client}>
<RequireAuth fallback={<p>Checking your session…</p>}>
<Account />
</RequireAuth>
</OAuth2Provider>
);
}RequireAuth starts sign-in when the session is unauthenticated. It starts none
while a logout() from useOAuth2() is signing out, so a sign-out button on a
protected page ends at the logout destination rather than the login page. The
guard covers only that hook: calling client.logout() directly bypasses it. If
the logout navigation never completes, for example a beforeunload handler
cancels it or the logout URL answers 204 or a download, the page stays on
fallback until it is reloaded. Use
useOAuth2() directly when a page should stay public and offer an explicit
sign-in button. Construct the client once per browser app, not on every render.
For server rendering, derive any initial auth state from the current request; do
not share a token-owning DirectClient across users.
With a HonoBff, bff.readSession(c) gives the server the answer
GET /auth/session gives the browser, without tokens. Pass it to the provider
as initialState, and a signed-in page renders signed in on the first paint
instead of waiting for the session probe:
import type { SessionState } from "@udibo/oauth2/client";
import { BffClient } from "@udibo/oauth2/client";
import { OAuth2Provider } from "@udibo/oauth2/react";
import type { ReactNode } from "react";
const client = new BffClient();
export function Root(
props: { session: SessionState; children: ReactNode },
) {
return (
<OAuth2Provider client={client} initialState={props.session}>
{props.children}
</OAuth2Provider>
);
}readSession applies the same session lifetime as the probe. In
sessionMode: "own", it destroys a session past sessionMaxAgeMs and clears
its cookie. Shared mode leaves lifetime enforcement to the application and its
session store. It sets no caching policy, so send a page rendered from its
answer with a private or no-store Cache-Control.
While signed in, the provider re-checks the session shortly before
sessionExpiresIn elapses. A BFF session whose access token has expired reports
sessionExpiresIn: 0 until a proxied call refreshes the token, so the provider
backs off rather than polling: it checks after 5 seconds, then doubles the wait
after each check that fails or reports no later expiry, up to one check every 5
minutes per tab. A check that reports a later expiry returns it to the
pre-expiry schedule, never more often than every 5 seconds. A BffClient.fetch that gets a 401 still signs the UI
out immediately.
A rendering guard does not protect data. Keep bearer/session validation,
required scopes, and record-level authorization on the server. Use the same
BffClient.fetch for credentialed API calls so the CSRF header is included.
Forms for an app that hosts its own login
Applications using Udibo redirect to hosted sign-in. Use the optional components below only on login pages your own application hosts. They provide fields and submission state; they do not create routes, sessions, or an identity service.
| Component | Use |
|---|---|
SignInForm, SignUpForm | App-owned password login and registration |
RequestPasswordResetForm, ResetPasswordForm | Request and complete a password reset |
MfaEnrollmentForm, MfaChallengeForm | Display enrollment and challenge controls |
UserMenu | User display and sign-out action |
import { SignInForm } from "@udibo/oauth2/react/components";
export function SignInPage() {
return (
<SignInForm
action="/auth/signin"
method="post"
onSubmit={async (values) => {
const response = await fetch("/auth/signin", {
method: "POST",
headers: { "content-type": "application/json", "x-csrf": "1" },
body: JSON.stringify(values),
});
if (!response.ok) {
return {
error: "Sign-in failed. Check your credentials.",
};
}
}}
onSuccess={() => location.assign("/")}
/>
);
}This assumes your application has mounted that sign-in endpoint. Validate the request and create a session in the backend's authentication hook. If MFA is enabled, route first-factor success to the challenge instead of immediately navigating to an authenticated page; see MFA.
onSubmit returns nothing for success or { error, fieldErrors } for a form
failure. onSuccess is a UI side effect, not an authorization gate; errors in
that callback are isolated. For a no-JavaScript form post, action/method
identify the backend route and that route must return the appropriate redirect.
Styling and custom markup
Components are unstyled. Use className, the per-slot classNames map, or
stable data-oauth2-* attributes. AuthFormClassNamesProvider supplies shared
form classes; an individual form can override a slot. UserMenu has its own
styling props.
While a submit is in flight, the submit button shows its pending label and
carries aria-disabled="true" instead of disabled, so it keeps keyboard
focus; repeat clicks, Enter, and Space are ignored until the submit settles.
Style the pending state with [data-oauth2-submit][aria-disabled="true"], not
:disabled. Custom markup built on useAuthForm gets the same guard from
handleSubmit; mark its button with
aria-disabled={form.isSubmitting || undefined}.
For full markup control, use useAuthForm or a component's render-prop
children. Keep labels, error associations, focus behavior, and autocomplete
attributes when replacing markup. The
component API lists the
slots and props.
See the React Router template for a complete app, and testing for mock providers and BFF session fixtures.

