FrontMCP Custom Authorization UI (auth.ui)
Entry point for replacing FrontMCP's built-in OAuth pages (login, consent, federated, incremental, error) with your own React components. Custom UI is a simple slot→file map (auth.ui) plus an extras name→handler map (auth.extras) on the auth config — there is no decorator and no class. The references/custom-auth-ui.md reference has the full API; the examples/ show a single login slot and a multi-step extras form.
When to Use This Skill
Must Use
- Branding or fully replacing the
local/remote mode login page with a custom React component
- Building a custom consent screen, federated provider picker, incremental authorization, or error page
- Adding a server-validated multi-step field to an authorization page (e.g. "add another item") via
auth.extras
Recommended
- Understanding which half is the server (
auth.ui / auth.extras in @frontmcp/sdk) and which is the client (@frontmcp/ui/auth)
- Looking up the
AuthFlowState fields a slot component receives, or the /oauth/ui/extra route
- Confirming that the framework (not your component) owns CSRF + CSP
Skip When
- You only need to add/rename fields on the built-in login page or run a custom verifier — use the declarative
login / authenticate config in frontmcp-config → configure-auth instead (no React, no build step)
- You are customizing a tool widget (not an auth page) — use
create-tool → ui-widgets
- You don't need a custom page at all — configuring no
auth.ui keeps the built-in pages
Decision: Use this skill when you want to render your OWN component for an auth slot. Use configure-auth's declarative login config when tweaking the built-in page's fields is enough.
Prerequisites
- A FrontMCP server in
local or remote auth mode (see frontmcp-config → configure-auth)
@frontmcp/ui, react, and react-dom installed (react/react-dom are peer deps of @frontmcp/ui)
Steps
- Write a React component (default export) for the slot, reading the injected state via
@frontmcp/ui/auth hooks (see references/custom-auth-ui.md)
- Map the slot to its
.tsx path under auth.ui — a RELATIVE path auto-anchored to the config file's directory (no fileURLToPath)
- (Optional) Add an
auth.extras[name] handler function for any mid-flow validated field
- Declare both under
auth: @FrontMcp({ auth: { mode: 'local', ui: { login: './…' }, extras: { … } } }) (per-app under splitByApp — put them on the @App({ auth }) that owns the pages)
- Verify using the Verification Checklist below
Scenario Routing Table
| Scenario |
Reference |
Description |
| Replace any built-in auth page with a React component |
custom-auth-ui |
auth.ui: { slot: './file.tsx' }, the hooks, <AuthPageWrapper>, mountAuthPage |
| Add a server-validated mid-flow field (multi-step form) |
custom-auth-ui |
auth.extras: { name: handler }, the accumulator, useExtraField / useAddedItems |
| Look up the flow-state fields a component receives |
custom-auth-ui |
AuthFlowState field table (no PII) |
| Understand the served route and the framework-owned CSRF + CSP |
custom-auth-ui |
/oauth/ui/extra, the esm.sh import-map + inline module, security ownership |
The map form
Custom UI is a slot→file map on the auth config. Point each slot at a sibling .tsx/.jsx source (default export); the SDK transpiles it once server-side and inlines it as an ES module, with deps loaded from esm.sh via an import-map and mountAuthPage appended for you — exactly as @Tool({ ui }) leads with the FileSource { file } form:
// src/server.ts
import { App } from '@frontmcp/sdk'; // or @FrontMcp
@App({
auth: {
mode: 'local',
// slot → RELATIVE .tsx, auto-anchored to THIS config file's directory.
ui: { login: './auth/login.tsx' },
// extra name → handler fn (no class).
extras: { 'envs:add': async (input, ctx) => ({ ok: true, addedItems: [{ key: String(input.key) }] }) },
},
})
export default class Server {}
A relative auth.ui path resolves against the directory of the file that declares the @App/@FrontMcp config — captured automatically at decoration time. No fileURLToPath needed. Absolute paths pass through; on capture failure the framework falls back to process.cwd() with a warning.
Common Patterns
| Pattern |
Correct |
Incorrect |
Why |
| Slot registration |
auth: { ui: { login: './login.tsx' } } |
auth: { ui: [LoginAuthUi] } (array of classes) |
Custom UI is a slot→file MAP now — no decorator, no class |
| Path anchoring |
Relative './login.tsx' (auto-anchored to the config file) |
fileURLToPath(new URL('./login.tsx', import.meta.url)) |
The framework captures the config file's dir for you — manual anchoring is no longer needed |
| Extra registration |
auth: { extras: { 'envs:add': handlerFn } } |
auth: { extras: [AddEnvExtra] } (annotated class) |
Extras are an extra-name → handler-function MAP now |
| CSRF |
Let the hooks round-trip csrfToken |
Generating / checking a token in your component |
The server mints + verifies CSRF; your component must not |
| User identity |
User-typed fields live in your own <form> inputs |
Putting email/name into AuthFlowState |
AuthFlowState is PII-free by contract — it carries OAuth client ids + control fields only |
| Wrapper form |
Wrap UI in <AuthPageWrapper> (renders the finish <form> + hidden fields) |
Hand-rolling pending_auth_id / csrf hidden inputs |
The wrapper injects the control fields so a no-JS submit still works |
Verification Checklist
Troubleshooting
| Problem |
Cause |
Solution |
| Built-in page still shows |
Slot not in auth.ui, or the .tsx failed to transpile |
Confirm the slot is in auth: { ui: { … } } on the scope that owns the pages; check logs — a transpile error falls back to the built-in page |
Blank page / @frontmcp/ui/auth 404s in the browser |
@frontmcp/ui/auth isn't on esm.sh (unpublished monorepo) |
Publish @frontmcp/ui, OR map it to a locally-served ESM URL via @FrontMcp({ ui: { cdnOverrides } }) (see references/custom-auth-ui.md → Local dev / offline) |
ENOENT / component not found at runtime |
Path doesn't resolve from the config file's dir |
Use a path relative to the config file (auto-anchored), or an absolute path |
| Hooks throw "must be used inside …" |
Component rendered without <AuthPageWrapper> |
Mount via mountAuthPage(Component) (it wraps for you) or wrap manually in <AuthPageWrapper> |
| extras POST returns 400 (csrf) |
The submitted csrf ≠ the server-minted token |
Let the hooks send it — useExtraField / submitExtra attach pending_auth_id + csrf automatically |
Custom page can't import @frontmcp/ui/auth |
Package (or react/react-dom) not installed |
npm install @frontmcp/ui react react-dom |
Examples
Each reference has matching examples under examples/<reference>/:
custom-auth-ui
| Example |
Level |
Description |
login-slot |
Intermediate |
Replace the built-in login page with a custom React component via auth.ui: { login: './login.tsx' } and useAuthFlow. |
multi-step-auth-extra |
Advanced |
Add a server-validated multi-step field with auth.extras: { 'envs:add': fn }, useExtraField, and useAddedItems. |
Accessing This Skill
Skills are distributed as plain SKILL.md files plus a sibling references/
and examples/ tree, so consumers can pick whichever access mode fits:
| Mode |
How it works |
| Filesystem |
Read libs/skills/catalog/frontmcp-auth-ui/ directly from a clone of the catalog repo, or from a published @frontmcp/skills install. SKILL.md is the entry point. |
frontmcp CLI |
frontmcp skills list, frontmcp skills read frontmcp-auth-ui, frontmcp skills read frontmcp-auth-ui:references/<file>.md, frontmcp skills install frontmcp-auth-ui — no server required. |
MCP skill:// |
When a developer mounts this skill into their own FrontMCP server (@FrontMcp({ skills: [...] })), the SDK exposes it via SEP-2640 resources: skill://frontmcp-auth-ui/SKILL.md, skill://frontmcp-auth-ui/references/{file}.md, etc. The server's skill://index.json returns the SEP-2640 discovery document for everything mounted on it. |
The catalog itself is not an MCP server. The skill:// URIs only resolve
when a server has been configured to host this skill.
Reference
1---2name: frontmcp-auth-ui3description: Use when customizing, branding, or replacing the built-in FrontMCP OAuth pages (the login, consent, federated-select, incremental-authorization, and error pages) with your own React components. Covers the auth.ui slot-to-file map and auth.extras name-to-handler map on the auth config (no decorator, no class); the @frontmcp/ui/auth React hooks, the AuthPageWrapper component, and mountAuthPage (client-rendered via an esm.sh import-map plus a per-file server-side transform, with no bundling and no SSR); and the framework-owned CSRF and CSP. Triggers: custom login page, brand the consent screen, replace the OAuth UI, custom authorization UI, style the auth pages, multi-step login. The skill for CUSTOM AUTHORIZATION UI (distinct from auth config in frontmcp-config and permissions in frontmcp-authorities).4license: Apache-2.05---67# FrontMCP Custom Authorization UI (`auth.ui`)89Entry point for replacing FrontMCP's built-in OAuth pages (login, consent, federated, incremental, error) with your own React components. Custom UI is a simple **slot→file map** (`auth.ui`) plus an extras **name→handler map** (`auth.extras`) on the auth config — there is no decorator and no class. The `references/custom-auth-ui.md` reference has the full API; the `examples/` show a single login slot and a multi-step extras form.1011## When to Use This Skill1213### Must Use1415- Branding or fully replacing the `local`/`remote` mode **login** page with a custom React component16- Building a custom **consent** screen, **federated** provider picker, **incremental** authorization, or **error** page17- Adding a server-validated multi-step field to an authorization page (e.g. "add another item") via `auth.extras`1819### Recommended2021- Understanding which half is the server (`auth.ui` / `auth.extras` in `@frontmcp/sdk`) and which is the client (`@frontmcp/ui/auth`)22- Looking up the `AuthFlowState` fields a slot component receives, or the `/oauth/ui/extra` route23- Confirming that the framework (not your component) owns CSRF + CSP2425### Skip When2627- You only need to add/rename **fields** on the built-in login page or run a custom verifier — use the declarative `login` / `authenticate` config in `frontmcp-config` → `configure-auth` instead (no React, no build step)28- You are customizing a **tool widget** (not an auth page) — use `create-tool` → `ui-widgets`29- You don't need a custom page at all — configuring no `auth.ui` keeps the built-in pages3031> **Decision:** Use this skill when you want to render your OWN component for an auth slot. Use `configure-auth`'s declarative `login` config when tweaking the built-in page's fields is enough.3233## Prerequisites3435- A FrontMCP server in `local` or `remote` auth mode (see `frontmcp-config` → `configure-auth`)36- `@frontmcp/ui`, `react`, and `react-dom` installed (`react`/`react-dom` are peer deps of `@frontmcp/ui`)3738## Steps39401. Write a React component (default export) for the slot, reading the injected state via `@frontmcp/ui/auth` hooks (see `references/custom-auth-ui.md`)412. Map the slot to its `.tsx` path under `auth.ui` — a RELATIVE path auto-anchored to the config file's directory (no `fileURLToPath`)423. (Optional) Add an `auth.extras[name]` handler function for any mid-flow validated field434. Declare both **under `auth`**: `@FrontMcp({ auth: { mode: 'local', ui: { login: './…' }, extras: { … } } })` (per-app under `splitByApp` — put them on the `@App({ auth })` that owns the pages)445. Verify using the Verification Checklist below4546## Scenario Routing Table4748| Scenario | Reference | Description |49| -------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------ |50| Replace any built-in auth page with a React component | `custom-auth-ui` | `auth.ui: { slot: './file.tsx' }`, the hooks, `<AuthPageWrapper>`, `mountAuthPage` |51| Add a server-validated mid-flow field (multi-step form) | `custom-auth-ui` | `auth.extras: { name: handler }`, the accumulator, `useExtraField` / `useAddedItems` |52| Look up the flow-state fields a component receives | `custom-auth-ui` | `AuthFlowState` field table (no PII) |53| Understand the served route and the framework-owned CSRF + CSP | `custom-auth-ui` | `/oauth/ui/extra`, the esm.sh import-map + inline module, security ownership |5455## The map form5657Custom UI is a **slot→file map** on the auth config. Point each slot at a sibling `.tsx`/`.jsx` source (default export); the SDK transpiles it once server-side and inlines it as an ES module, with deps loaded from esm.sh via an import-map and `mountAuthPage` appended for you — exactly as `@Tool({ ui })` leads with the `FileSource` `{ file }` form:5859```ts60// src/server.ts61import { App } from '@frontmcp/sdk'; // or @FrontMcp6263@App({64 auth: {65 mode: 'local',66 // slot → RELATIVE .tsx, auto-anchored to THIS config file's directory.67 ui: { login: './auth/login.tsx' },68 // extra name → handler fn (no class).69 extras: { 'envs:add': async (input, ctx) => ({ ok: true, addedItems: [{ key: String(input.key) }] }) },70 },71})72export default class Server {}73```7475A relative `auth.ui` path resolves against the directory of the file that declares the `@App`/`@FrontMcp` config — captured automatically at decoration time. **No `fileURLToPath` needed.** Absolute paths pass through; on capture failure the framework falls back to `process.cwd()` with a warning.7677## Common Patterns7879| Pattern | Correct | Incorrect | Why |80| ------------------ | ---------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------- |81| Slot registration | `auth: { ui: { login: './login.tsx' } }` | `auth: { ui: [LoginAuthUi] }` (array of classes) | Custom UI is a slot→file MAP now — no decorator, no class |82| Path anchoring | Relative `'./login.tsx'` (auto-anchored to the config file) | `fileURLToPath(new URL('./login.tsx', import.meta.url))` | The framework captures the config file's dir for you — manual anchoring is no longer needed |83| Extra registration | `auth: { extras: { 'envs:add': handlerFn } }` | `auth: { extras: [AddEnvExtra] }` (annotated class) | Extras are an extra-name → handler-function MAP now |84| CSRF | Let the hooks round-trip `csrfToken` | Generating / checking a token in your component | The server mints + verifies CSRF; your component must not |85| User identity | User-typed fields live in your own `<form>` inputs | Putting email/name into `AuthFlowState` | `AuthFlowState` is PII-free by contract — it carries OAuth client ids + control fields only |86| Wrapper form | Wrap UI in `<AuthPageWrapper>` (renders the finish `<form>` + hidden fields) | Hand-rolling `pending_auth_id` / `csrf` hidden inputs | The wrapper injects the control fields so a no-JS submit still works |8788## Verification Checklist8990- [ ] `GET /oauth/authorize` returns a page with an EMPTY `#frontmcp-auth-root` (not the built-in page) — the component's rendered markup is NOT in the HTTP response91- [ ] The page has a `<script type="module">` with the TRANSPILED component (`React.createElement`, NOT a bundle/IIFE/`react-dom/server`) + an `import { mountAuthPage } from '@frontmcp/ui/auth'` tail92- [ ] The page has a `<script type="importmap">` mapping `react` + `@frontmcp/ui/auth` → `https://esm.sh/...`, with `?external=react,react-dom` on the `@frontmcp/*` URLs (single React)93- [ ] The HTML injects `window.__FRONTMCP_AUTH__` with the flow state (and no PII — no `email`/`name` field)94- [ ] Response carries the auth CSP headers (`frame-ancestors 'none'`, `https://esm.sh` allowed, NO `'unsafe-eval'`) and `X-Frame-Options: DENY`95- [ ] There is **no** `/oauth/ui/<slot>.js` route (it 404s — the module is inlined, not served separately)96- [ ] (If using `auth.extras`) a valid `POST /oauth/ui/extra` returns `{ ok: true, addedItems }`, an invalid one returns 400, and a bad `csrf` returns 40097- [ ] Removing the slot from `auth.ui` falls back to the built-in page unchanged9899## Troubleshooting100101| Problem | Cause | Solution |102| ---------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |103| Built-in page still shows | Slot not in `auth.ui`, or the `.tsx` failed to transpile | Confirm the slot is in `auth: { ui: { … } }` on the scope that owns the pages; check logs — a transpile error falls back to the built-in page |104| Blank page / `@frontmcp/ui/auth` 404s in the browser | `@frontmcp/ui/auth` isn't on esm.sh (unpublished monorepo) | Publish `@frontmcp/ui`, OR map it to a locally-served ESM URL via `@FrontMcp({ ui: { cdnOverrides } })` (see `references/custom-auth-ui.md` → Local dev / offline) |105| `ENOENT` / component not found at runtime | Path doesn't resolve from the config file's dir | Use a path relative to the config file (auto-anchored), or an absolute path |106| Hooks throw "must be used inside …" | Component rendered without `<AuthPageWrapper>` | Mount via `mountAuthPage(Component)` (it wraps for you) or wrap manually in `<AuthPageWrapper>` |107| extras POST returns 400 (csrf) | The submitted `csrf` ≠ the server-minted token | Let the hooks send it — `useExtraField` / `submitExtra` attach `pending_auth_id` + `csrf` automatically |108| Custom page can't import `@frontmcp/ui/auth` | Package (or `react`/`react-dom`) not installed | `npm install @frontmcp/ui react react-dom` |109110## Examples111112Each reference has matching examples under [`examples/<reference>/`](./examples/):113114### `custom-auth-ui`115116| Example | Level | Description |117| ----------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------ |118| [`login-slot`](./examples/custom-auth-ui/login-slot.md) | Intermediate | Replace the built-in login page with a custom React component via `auth.ui: { login: './login.tsx' }` and `useAuthFlow`. |119| [`multi-step-auth-extra`](./examples/custom-auth-ui/multi-step-auth-extra.md) | Advanced | Add a server-validated multi-step field with `auth.extras: { 'envs:add': fn }`, `useExtraField`, and `useAddedItems`. |120121## Accessing This Skill122123Skills are distributed as plain SKILL.md files plus a sibling `references/`124and `examples/` tree, so consumers can pick whichever access mode fits:125126| Mode | How it works |127| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |128| **Filesystem** | Read `libs/skills/catalog/frontmcp-auth-ui/` directly from a clone of the catalog repo, or from a published `@frontmcp/skills` install. SKILL.md is the entry point. |129| **`frontmcp` CLI** | `frontmcp skills list`, `frontmcp skills read frontmcp-auth-ui`, `frontmcp skills read frontmcp-auth-ui:references/<file>.md`, `frontmcp skills install frontmcp-auth-ui` — no server required. |130| **MCP `skill://`** | When a developer mounts this skill into their own FrontMCP server (`@FrontMcp({ skills: [...] })`), the SDK exposes it via SEP-2640 resources: `skill://frontmcp-auth-ui/SKILL.md`, `skill://frontmcp-auth-ui/references/{file}.md`, etc. The server's `skill://index.json` returns the SEP-2640 discovery document for everything mounted on it. |131132The catalog itself is **not** an MCP server. The `skill://` URIs only resolve133when a server has been configured to host this skill.134135## Reference136137- [Custom Authorization UI (`auth.ui`)](https://docs.agentfront.dev/frontmcp/authentication/custom-ui)138- Related skills: `frontmcp-config` (→ `configure-auth` for the declarative `login` config), `create-tool` (→ `ui-widgets` for tool widgets)