Skip to content
Back to skills

SecuringAppsReactMcp

ASecurity

Wire Keycloak login into a React SPA with oidc-spa, and register or repair the OIDC client the app needs, driven through the Keycloak MCP server's tools. Use this whenever someone wants login added to a browser app - login/logout controls, route protection, reading the signed-in user - against a Keycloak realm. Covers the oidc-spa v10 builder API (which replaced the removed createReactOidc), why the registered redirect URI needs a trailing slash, why updating an existing client must be a merg...

  • 4 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 15, 2026
ai-agentsgoreactapi

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned October 15, 2026

npx -y skills add p2-inc/keycloak-skills --skill securingAppsReactMcp --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of SecuringAppsReactMcp?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for SecuringAppsReactMcp
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/p2-inc-securingappsreactmcp/badge)](https://www.skillsdirectory.com/skills/p2-inc-securingappsreactmcp)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: securingAppsReactMcp
description: >-
  Wire Keycloak login into a React SPA with oidc-spa, and register or repair the OIDC client the
  app needs, driven through the Keycloak MCP server's tools. Use this whenever someone wants login
  added to a browser app - login/logout controls, route protection, reading the signed-in user -
  against a Keycloak realm. Covers the oidc-spa v10 builder API (which replaced the removed
  createReactOidc), why the registered redirect URI needs a trailing slash, why updating an
  existing client must be a merge rather than a rebuild, and why web origins are a separate
  setting from redirect URIs that fails only in a real browser.
---

<!-- Copyright 2026 Phase Two, Inc. -->
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->

# React + Keycloak login — via the Keycloak MCP server

## Step 0 — Is the MCP surface actually here?

**Do this before any tool call.** Check your tool list once, now, for:

`createOidcClient` · `deleteClient` · `listClients` · `updateOidcClient`

- **All present** → say "Using the Keycloak MCP tools", then follow this skill as written.
- **Any missing** → say "Keycloak MCP tools unavailable in this session (missing: …) — using the
  Keycloak admin REST API instead", then **keep reading**. Every stage below still applies: the
  behaviour it configures is a property of Keycloak, not of the transport. Only the calls change —
  carry out each stage against the admin REST API (`$KC_URL/admin/realms/{realm}/...`) with an
  admin token. If the sibling skill `securingAppsReactRest` is loaded, use its exact REST recipes.

Decide once, here. Do not call an MCP tool to discover whether it exists, and do not alternate
between the two paths afterwards — discovering the gap a step at a time is measurably more
expensive than committing up front.

Say which path you took either way: a run that silently swaps tooling is indistinguishable from
one that never had the tools.

## What this is

A browser-only React app logging users into Keycloak with authorization code + PKCE, using
[`oidc-spa`](https://docs.oidc-spa.dev/). The client is **public** — a browser bundle cannot hold a
secret, so there is none, and PKCE replaces it.

Two halves, both required: **the app's source**, and **the client registration in Keycloak**. A
correct app against a stale client does not log anyone in.

## Half 1: the app

**Verified against `oidc-spa` 10.2.11.** `oidcSpa` is a **builder**, not a config function.

`src/oidc.ts`:

```ts
import { oidcSpa } from "oidc-spa/react-spa";
import { z } from "zod";

export const {
    bootstrapOidc, useOidc, getOidc, withLoginEnforced, OidcInitializationGate
} = oidcSpa
    .withExpectedDecodedIdTokenShape({
        decodedIdTokenSchema: z.object({
            sub: z.string(),
            name: z.string().optional(),
            preferred_username: z.string().optional()
        })
    })
    .createUtils();

bootstrapOidc({
    implementation: "real",
    issuerUri: "http://localhost:8080/auth/realms/<realm>",   // INCLUDES /realms/<realm>
    clientId: "<client-id>"
});
```

`src/main.tsx` — the wrapper is `OidcInitializationGate`. **There is no `OidcProvider` in v10.**

```tsx
<OidcInitializationGate>
    <App />
</OidcInitializationGate>
```

Reading state, login, logout:

```tsx
const { isUserLoggedIn } = useOidc();
const { login } = useOidc({ assert: "user not logged in" });
const { decodedIdToken, logout } = useOidc({ assert: "user logged in" });
// <button onClick={() => login()}>   /   <button onClick={() => logout({ redirectTo: "home" })}>
```

Claims live on `decodedIdToken`. Access token for API calls:
`await (await getOidc()).getAccessToken()`.

> **If you remember a different API, you are remembering v8.** `createReactOidc` and the whole
> `oidc-spa/react` entry point were **removed in v9**. `createOidcProvider` / `createUseOidc` are
> v3-era. Importing any of them fails to resolve.

PKCE is automatic and cannot be disabled — the library enforces `S256`.

## Half 2: the client

Tools: `listClients` to see what exists, `createOidcClient` for a new one, `updateOidcClient` to
repair one that already exists. Capture `deploymentId` and `deploymentRealm` and reuse them.

**Check first, with `listClients`.** If the app's client already exists, you are updating it.

```
updateOidcClient(deploymentId, deploymentRealm,
                 clientId     = "<client-id>",
                 redirectUris = "<full intended list>",
                 webOrigins   = "<full intended list>")
```

Three things that decide whether this works:

1. **The redirect URI ends with a trailing slash.** oidc-spa returns to the app's *base URL*, so a
   Vite app on port 5173 needs `http://localhost:5173/` registered — with the slash. Keycloak
   matches redirect URIs exactly and rejects the callback outright without it
   (`Invalid parameter: redirect_uri`, before the login form).

2. **Web origins are a SEPARATE setting from redirect URIs, and they fail differently.** The
   redirect URI authorizes where Keycloak sends the user *back*; the web origin authorizes the
   **CORS** origin of the browser's *token* call. Register only the redirect URI and login appears
   to succeed, then the token request is blocked in the browser — while every scripted,
   server-side test passes, because nothing in a script triggers a CORS preflight. Set
   `webOrigins` to the app's origin (`http://localhost:5173`, no trailing slash) or `"+"`.

3. **Both lists REPLACE, they do not append.** Read the current values from `listClients` first and
   send the full intended list, or you drop whatever was already registered.

**Never `deleteClient` a live client to change an attribute.** Deleting and re-creating destroys
its protocol mappers, role mappings, secret and sessions — a rebuilt client can log a user in while
having silently lost configuration other systems depend on. `updateOidcClient` merges: it reads the
current representation and changes only what you pass, because Keycloak's underlying PUT replaces
the whole client.

Also confirm the client is `public` with standard flow enabled, and that PKCE is required
(`S256`).

## Verify

1. `npm run build` exits 0.
2. `listClients` shows the client enabled, public, with both the redirect URI and the web origin.
3. A real browser login from the app's origin completes **and** the token call succeeds — that last
   part is the half a scripted test cannot see.

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `Invalid parameter: redirect_uri` before the login form | trailing slash missing | register `http://host:port/` |
| Login works, then a CORS error on the token call | `webOrigins` not set | add the origin, or `"+"` |
| `Cannot find module 'oidc-spa/react'` | v8 import on a v9/v10 install | use `oidc-spa/react-spa` |
| `decodedIdToken` undefined | read without `assert` | narrow with `assert: "user logged in"` |
| Config that existed before the change is gone | client was rebuilt, not updated | `updateOidcClient`, never delete-and-re-create |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.