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.
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 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) con
app_typespaytoken_endpoint_auth_methodnone(un cliente público no tiene secreto). En un entornodevelopment, 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.
- el URI de redirección
- Una API registrada en el entorno (Crear una API) con el ámbito
messages:read. Suidentifier, por ejemplohttps://api.example.com, es la audiencia de sus tokens de acceso. La guía de inicio rápido de Express crea una API que acepta estos tokens. - Node.js 22.18 o posterior.
1. Instalar
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:
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
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:
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.
<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:
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:
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:
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:
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
<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:
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.
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");
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: todas las opciones, dónde se guardan los tokens y los límites actuales.
- SDK de React: componentes integrados de inicio de sesión, registro y cuenta.
- Proteger una API de Express: la API a la que llama esta aplicación.