Añadir inicio de sesión a una aplicación Next.js

Este inicio rápido añade el inicio de sesión de KeenAuth a una aplicación Next.js con App Router, usando el SDK @keenauth/nextjs. Su aplicación es el cliente OAuth y guarda todos los tokens en el servidor; el navegador recibe solo una cookie de sesión cifrada y HttpOnly. Usted añade las rutas de inicio de sesión, protege una página, llama a una API protegida con el token de acceso del usuario y prueba la aplicación sin conexión. App Router es el único enrutador que admite el SDK. El SDK se describe en SDK de Next.js.

La aplicación tiene estas páginas y rutas:

Ruta Qué hace
/ Página de inicio pública: un enlace para iniciar sesión, o el nombre del usuario y un botón para cerrar sesión
/dashboard Requiere una sesión. Muestra el usuario y los mensajes que devuelve su API
/api/messages Requiere una sesión. Llama a su API con el token de acceso del usuario
/auth/login, /auth/callback, /auth/logout Servidas por el SDK

Antes de empezar

1. Instalar

npm install @keenauth/nextjs next@16.4.2 react@19.3.1 react-dom@19.3.1

El SDK todavía no está publicado. Hasta entonces, el ejemplo enlaza con el código fuente del SDK en su package.json.

2. Configurar

La aplicación lee su configuración solo desde variables de entorno. Colóquelas en .env.local, que Next.js carga, y no suba nunca ese archivo al control de versiones.

Variable Obligatoria Significado
KEENAUTH_ISSUER sí El emisor de su entorno
KEENAUTH_CLIENT_ID sí El client_id de la aplicación web
KEENAUTH_CLIENT_KEY una de las dos La clave privada (una JWK privada en JSON) para private_key_jwt, recomendada
KEENAUTH_CLIENT_SECRET una de las dos Un secreto de cliente
APP_BASE_URL sí La URL pública de la aplicación. El SDK construye desde ella el URI de redirección y todas las redirecciones. http solo para localhost, 127.0.0.1 o [::1]
KEENAUTH_COOKIE_SECRET sí Al menos 32 caracteres; sella la cookie de sesión. Genere uno con openssl rand -base64 32
API_URL sí La API a la que llama la aplicación, por ejemplo http://localhost:8080
KEENAUTH_API_AUDIENCE sí El identificador de esa API en KeenAuth

loadConfig lee y comprueba estos valores. Se niega a continuar si falta alguno o si se definen ambas credenciales de cliente o ninguna, y sus mensajes de error nunca contienen un secreto.

export function loadConfig(env: Record<string, string | undefined>): AppConfig {
  const need = (name: string): string => {
    const value = env[name];
    if (!value) throw new Error(`${name} is required`);
    return value;
  };

3. Crear la instancia del SDK

createAuth crea la instancia del SDK, y el SDK comprueba cada opción en el momento de crearla. getApp crea una instancia por proceso en el primer uso, de modo que next build no necesita secretos.

/** Builds the SDK instance. The options are checked here, so a configuration error stops the app at start-up. */
export function createAuth(config: AppConfig, overrides: Partial<KeenAuthOptions> = {}): KeenAuth {
  return createKeenAuth({ ...config.auth, ...overrides });
}

let instance: { auth: KeenAuth; config: AppConfig } | undefined;

/** One instance per process, created on first use so that `next build` needs no secrets. */
export function getApp(): { auth: KeenAuth; config: AppConfig } {
  if (!instance) {
    const config = loadConfig(process.env);
    instance = { auth: createAuth(config), config };
  }
  return instance;
}

La configuración establece resource con el identificador de su API (RFC 8707). Sin él, el token de acceso no está destinado a su API y la API lo rechaza.

4. Añadir las rutas de inicio de sesión

Un único controlador de ruta comodín sirve /auth/login, /auth/callback y /auth/logout:

// Serves /auth/login, /auth/callback and /auth/logout.
export const GET = (request: Request) => getAuth().handlers.GET(request);
export const POST = (request: Request) => getAuth().handlers.POST(request);

El inicio de sesión usa el flujo de código de autorización con PKCE y una solicitud de autorización enviada previamente (pushed authorization request) cuando su entorno la ofrece. El callback comprueba state, iss y el ID token. Un returnTo que no sea una ruta de su aplicación se ignora.

5. Proteger páginas con el proxy

proxy.ts (middleware.ts antes de Next.js 16) renueva la sesión antes de que se renderice una página y protege las rutas que usted elija. Una solicitud de página sin sesión se redirige a /auth/login con returnTo; una solicitud a /api/… recibe 401.

export async function proxy(request: NextRequest) {
  const result = await getAuth().proxy(request, { protect: isProtected });
  if (result.response) return result.response;
  const response = NextResponse.next({ request: { headers: result.requestHeaders } });
  result.apply(response);
  return response;
}

Las rutas que requieren una sesión están en una sola función:

export function isProtected(pathname: string): boolean {
  return pathname === "/dashboard" || pathname.startsWith("/dashboard/") || pathname.startsWith("/api/");
}

El patrón del proxy puede no cubrir una ruta, de modo que cada página y cada controlador de ruta que necesite un usuario también comprueba la sesión por sí mismo.

6. Llamar a su API

En un Server Component, lea la sesión y tome el token de acceso del almacén de cookies. El token permanece en el servidor; el navegador recibe solo el HTML.

  // The proxy has already sent users without a session to the sign-in; check again here, because a matcher can miss a path.
  const session = await auth.getSession(cookieStore);
  if (!session) return <a href="/auth/login?returnTo=/dashboard">Sign in</a>;

  // The access token stays on the server. The browser receives only the HTML below.
  const response = await fetchMessages(config.apiUrl, await auth.getAccessToken(cookieStore));

session.user contiene las notificaciones del ID token, como subject, email y name. La sesión nunca contiene un token.

La llamada en sí es un fetch normal con un token de portador:

export async function fetchMessages(apiUrl: string, accessToken: string, fetchImpl: typeof fetch = fetch): Promise<Response> {
  return fetchImpl(new URL("/api/messages", apiUrl), {
    headers: { Authorization: `Bearer ${accessToken}` },
    cache: "no-store",
  });
}

Un controlador de ruta también puede establecer cookies, así que usa updateSession, que renueva un token de acceso próximo a caducar:

  // A route handler can set cookies, so updateSession also refreshes an access token that is about to expire.
  const { accessToken, apply } = await auth.updateSession(request);
  if (!accessToken) return new Response(null, { status: 401 });

No pase el token de acceso a un Client Component.

7. Cerrar sesión

Cerrar sesión es un formulario que envía una solicitud POST a /auth/logout. El SDK elimina la cookie de sesión, revoca el refresh token y envía el navegador al endpoint de cierre de sesión de su entorno, que lo devuelve a su aplicación. Una solicitud que el navegador marca como procedente de otro sitio se rechaza.

      <form action="/auth/logout" method="post">
        <button type="submit">Sign out</button>
      </form>

8. Ejecutar la aplicación

Inicie la API del inicio rápido de Express (o cualquier API que acepte los tokens) y ejecute la aplicación:

npm run dev

Abra http://localhost:3000, elija Sign in, inicie sesión en KeenAuth y abra el panel.

9. Probar sin red

Las pruebas no necesitan red ni una cuenta de KeenAuth. Un fetch simulado hace de servidor de autorización (descubrimiento y la solicitud de autorización enviada previamente), y Node ejecuta directamente las pruebas en TypeScript.

test("proxy: a page request without a session is redirected to sign-in with returnTo", async () => {
  const { auth } = app();
  const result = await auth.proxy(new Request(`${APP}/dashboard?tab=1`), { protect: isProtected });
  assert.equal(result.response?.status, 307);
npm test
npm run deps    # pinned versions, lockfile sources and licences

Las pruebas comprueban que la configuración se valida al arrancar, que una página sin sesión redirige al inicio de sesión y que una ruta de API responde 401, que el inicio de sesión envía una solicitud PKCE S256 enviada previamente y establece una cookie HttpOnly sellada, que se rechaza un callback con un state desconocido, que se rechaza un cierre de sesión desde otro sitio y que el token de acceso llega a la API en la cabecera Authorization.

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

Detrás de un proxy o balanceador de carga

Defina APP_BASE_URL con la URL pública de la aplicación. El SDK nunca lee Host ni X-Forwarded-* para construir redirecciones. Con varias instancias del servidor, dos de ellas podrían renovar a la vez el mismo refresh token; consulte Límites actuales.

Próximos pasos