
# Añadir inicio de sesión a una aplicación React de página única

Esta guía de inicio rápido añade el inicio de sesión de KeenAuth a una aplicación React de página única creada con Vite, usando el SDK para navegador `@keenauth/js`. La aplicación permite que los usuarios inicien sesión mediante una redirección, llama a una API protegida con un token de acceso vinculado a DPoP y permite cerrar sesión. También puede probarla sin conexión. El SDK se describe en [SDK de JavaScript para navegador](/docs/sdk/js).

En una aplicación de página única, los tokens se guardan en el navegador. El SDK los mantiene solo en memoria y los vincula a una clave no extraíble mediante DPoP, de modo que un token robado no puede usarse sin esa clave. Si su aplicación tiene un servidor, la [guía de inicio rápido de Next.js](/docs/quickstarts/nextjs) mantiene todos los tokens en el servidor (backend for frontend), que es la opción más segura.

La aplicación tiene estas rutas:

| Ruta | Qué hace |
|---|---|
| `/` | Un botón de inicio de sesión o, si el usuario ya ha iniciado sesión, el usuario, un botón que llama a su API y un botón de cierre de sesión |
| `/callback` | El URI de redirección. El SDK completa aquí el inicio de sesión y la aplicación vuelve a la página desde la que el usuario comenzó |

## Antes de empezar

- Un entorno de KeenAuth. Su emisor tiene un aspecto como `https://your-environment.eu.keenauth.app`.
- Una aplicación de página única en el entorno ([Crear una aplicación](/docs/api/management/applications/create)) con `app_type` `spa` y `token_endpoint_auth_method` `none` (un cliente público no tiene secreto). En un entorno `development`, registre:
  - el URI de redirección `http://localhost/callback` (un URI de redirección de loopback se registra sin puerto y coincide con cualquier puerto);
  - el URI de redirección tras cerrar sesión `http://localhost/`;
  - el origen web permitido `http://localhost:5173`, para que el navegador pueda llamar al endpoint de token.
- Una API registrada en el entorno ([Crear una API](/docs/api/management/apis/create)) con el ámbito `messages:read`. Su `identifier`, por ejemplo `https://api.example.com`, es la audiencia de sus tokens de acceso. La [guía de inicio rápido de Express](/docs/quickstarts/express) crea una API que acepta estos tokens.
- Node.js 22.18 o posterior.

## 1. Instalar

```sh
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install @keenauth/js
```

El SDK todavía no está publicado. Hasta que lo esté, el ejemplo enlaza con el código fuente del SDK en su `package.json`. El ejemplo fija `vite@8.3.4`, `@vitejs/plugin-react@6.1.2`, `react@19.3.1` y `react-dom@19.3.1`.

## 2. Configurar

La aplicación lee su configuración solo desde variables de entorno. Colóquelas en `.env.local`, que Vite carga, y nunca envíe ese archivo al control de versiones. Vite incluye en el JavaScript que descarga el navegador todas las variables `VITE_`, así que no coloque ahí ningún secreto. Esta aplicación no tiene ninguno.

| Variable | Obligatoria | Significado |
|---|---|---|
| `VITE_KEENAUTH_ISSUER` | sí | El emisor de su entorno |
| `VITE_KEENAUTH_CLIENT_ID` | sí | El `client_id` de la aplicación de página única |
| `VITE_KEENAUTH_API_AUDIENCE` | sí | El identificador de su API en KeenAuth |
| `VITE_API_URL` | no | URL base de su API. Vacío significa el mismo origen que la aplicación |
| `API_PROXY_TARGET` | no | Solo para desarrollo: adónde reenvía el servidor de desarrollo `/api`; por defecto `http://localhost:8080` |

`loadConfig` lee y comprueba estos valores. Si falta alguno, detiene la aplicación con un error que indica el nombre de la variable:

```ts
export function loadConfig(env: Readonly<Record<string, unknown>>): AppConfig {
  const need = (name: string): string => {
    const v = env[name];
    if (typeof v !== "string" || v.trim() === "") throw new Error(`${name} is required (see .env.example)`);
    return v.trim();
  };
```

## 3. Crear el cliente del SDK

```ts
export function createAuth(config: AppConfig, origin: string, inject: Injectables = {}): KeenAuth {
  return new KeenAuth({
    issuer: config.issuer,
    clientId: config.clientId,
    redirectUri: `${origin}${CALLBACK_PATH}`,
    postLogoutRedirectUri: `${origin}/`,
    // The access token is for this API: its aud is the API identifier (RFC 8707).
    resource: config.apiAudience,
    // messages:read is a scope of that API; the example API requires it on /api/messages.
    scope: "openid profile email offline_access messages:read",
    ...inject,
  });
}
```

`resource` es el identificador de su API (RFC 8707). El SDK lo envía en la solicitud de autorización, en el intercambio del código y en cada renovación, y la audiencia del token de acceso es su API. Sin este valor, el token de acceso no está destinado a su API y la API lo rechaza. El SDK comprueba cada opción al crear el cliente; un emisor o un `resource` no válidos lanzan un `KeenAuthError` con el código `config_invalid`.

Cree un único cliente para la página y páselo a un proveedor:

```tsx
const config = loadConfig(import.meta.env);
const auth = createAuth(config, window.location.origin);

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <AuthProvider auth={auth}>
      <App apiUrl={config.apiUrl} />
    </AuthProvider>
  </StrictMode>,
);
```

## 4. Iniciar sesión y gestionar la redirección de vuelta

El botón de inicio de sesión inicia el inicio de sesión por redirección: flujo de código de autorización con PKCE (S256), `state` y `nonce`. `appState` es la ruta a la que volver.

```tsx
        <button onClick={() => auth.signInRedirect({ appState: window.location.pathname })}>Sign in</button>
```

Cuando KeenAuth redirige de vuelta a `/callback`, el proveedor completa el inicio de sesión. El SDK comprueba `state`, `iss` y el ID token, y canjea el código con una prueba DPoP. Una respuesta de autorización solo puede usarse una vez, y `StrictMode` de React ejecuta los efectos dos veces en desarrollo, así que `completeSignIn` recuerda el primer resultado:

```ts
export function completeSignIn(auth: KeenAuth, href: string): Promise<string | null> {
  const url = new URL(href);
  if (url.pathname !== CALLBACK_PATH || !(url.searchParams.has("code") || url.searchParams.has("error"))) {
    return Promise.resolve(null);
  }
  let done = completions.get(auth);
  if (!done) {
    done = auth.handleRedirectCallback(href).then((r) => safeReturnPath(r.appState));
    completions.set(auth, done);
  }
  return done;
}
```

Después, el proveedor elimina `code` y `state` de la barra de direcciones. Solo se usa como ruta de retorno una ruta dentro de su aplicación, así que un `appState` manipulado no puede enviar al usuario a otro sitio:

```ts
export function safeReturnPath(path: string | undefined): string {
  return path !== undefined && /^\/(?![/\\])/.test(path) ? path : "/";
}
```

Sirva también `index.html` para `/callback` en producción (el servidor de desarrollo de Vite ya lo hace).

## 5. Llamar a su API

`authorizedFetch` envía `Authorization: DPoP <token>` con una prueba nueva vinculada al token, y renueva el token cuando está a punto de caducar:

```ts
export async function fetchMessages(auth: KeenAuth, apiUrl: string): Promise<unknown> {
  const res = await auth.authorizedFetch(`${apiUrl}/api/messages`, { headers: { Accept: "application/json" } });
  if (!res.ok) throw new Error(`The API answered ${res.status}`);
  return res.json();
}
```

En desarrollo, el servidor de Vite reenvía `/api` a su API, de modo que el navegador la llama desde el mismo origen que la aplicación y la API no necesita CORS:

```ts
      proxy: { "/api": env.API_PROXY_TARGET || "http://localhost:8080" },
```

La API comprueba la prueba DPoP frente a la URL que llamó el navegador. Si ejecuta la API de la guía de inicio rápido de Express detrás de este proxy, establezca su `PUBLIC_BASE_URL` en `http://localhost:5173`. En producción, sirva la API desde el mismo origen o habilite CORS para el origen de su aplicación, y establezca `VITE_API_URL`.

## 6. Cerrar sesión

```tsx
      <button onClick={() => auth.signOut()}>Sign out</button>
```

`signOut` elimina los tokens, revoca el token de renovación y envía el navegador al endpoint de cierre de sesión de su entorno, que lo devuelve a `/`.

## 7. Ejecutar la aplicación

Inicie la API de la guía de inicio rápido de Express con `PUBLIC_BASE_URL=http://localhost:5173` y `KEENAUTH_AUDIENCE` establecido con el identificador de su API, y después ejecute la aplicación:

```sh
cp .env.example .env.local   # then fill it in
npm run dev
```

Abra `http://localhost:5173`, elija **Sign in**, inicie sesión en KeenAuth y elija **Call the API**.

El SDK mantiene los tokens solo en memoria. Tras recargar la página, el usuario queda desconectado en la aplicación y debe iniciar sesión de nuevo; la sesión de KeenAuth normalmente hace que esto sea una redirección sin solicitud de credenciales.

## 8. Probar sin red

Las pruebas no necesitan red ni una cuenta de KeenAuth. Un `fetch` simulado hace de servidor de autorización y de API, y el ID token se firma con una clave creada para la prueba. Node ejecuta directamente las pruebas en TypeScript.

```ts
  assert.deepEqual(await fetchMessages(s.auth, ""), { messages: ["hello"] });
  const call = s.seen.find((c) => c.url === `${ORIGIN}/api/messages`)!;
  assert.equal(call.headers.get("Authorization"), "DPoP access-token-1");
```

```sh
npm test
npm run typecheck
npm run deps    # pinned versions, lockfile sources and licences
npm run build
```

Las pruebas comprueban que la configuración se valida, que el SDK rechaza un emisor o un `resource` no válidos, que el inicio de sesión envía PKCE S256 y su API como `resource`, que el callback se completa una sola vez y envía `resource` junto con el código, que un `state` falsificado se rechaza antes de cualquier solicitud de token, que tras el inicio de sesión solo se usa una ruta dentro de la aplicación, que ningún token se escribe en el almacenamiento y que la llamada a la API lleva el token vinculado a DPoP y su prueba.

El inicio de sesión completo con un navegador real necesita un entorno de KeenAuth. No forma parte de las pruebas sin conexión.

## Próximos pasos

- [SDK de JavaScript para navegador](/docs/sdk/js): todas las opciones, dónde se guardan los tokens y los límites actuales.
- [SDK de React](/docs/sdk/react): componentes integrados de inicio de sesión, registro y cuenta.
- [Proteger una API de Express](/docs/quickstarts/express): la API a la que llama esta aplicación.
