
# Sesiones de Go net/http

El paquete `github.com/keenauth/keenauth-go/bff` añade el inicio de sesión a una aplicación Go `net/http`. Su servidor es el cliente OAuth y conserva todos los tokens en el servidor. El navegador solo recibe una cookie de sesión cifrada y HttpOnly, y nunca ve un token de acceso, de actualización ni de ID. Este es el patrón backend-for-frontend (BFF). El paquete usa solo la biblioteca estándar. Todavía no está publicado.

Para proteger una API que recibe tokens de portador, consulte [Protección de su API](/docs/sdk/protect-api).

## Antes de empezar

1. En el panel, cree una aplicación web en su entorno.
2. Elija la autenticación del cliente: un secreto de cliente (`bff.ClientSecretBasic`) o `private_key_jwt` (`bff.PrivateKeyJWT`). Para `private_key_jwt`, registre la clave pública de la aplicación y mantenga la clave privada en su servidor.
3. Añada el URI de redirección `https://app.acme.example/auth/callback` y el URI de redirección tras cerrar sesión `https://app.acme.example/`.
4. Genere un secreto para la cookie de al menos 32 caracteres, por ejemplo con `openssl rand -base64 32`. Guárdelo en su almacén de secretos, no en el repositorio.

## Configuración

```go
package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/keenauth/keenauth-go/bff"
)

func main() {
	auth, err := bff.New(bff.Options{
		Issuer:     "https://acme.eu.keenauth.app", // your environment's issuer, exactly
		ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
		ClientAuth: bff.ClientSecretBasic{Secret: os.Getenv("KEENAUTH_CLIENT_SECRET")},
		AppBaseURL: "https://app.acme.example",
		Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
	})
	if err != nil {
		panic(err) // configuration errors stop the app at start-up
	}

	mux := http.NewServeMux()
	mux.Handle("GET /orders", auth.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s := bff.FromContext(r.Context()).Session
		fmt.Fprintf(w, "Signed in as %s", s.User.Email)
	})))

	http.ListenAndServe(":8080", auth.Middleware(mux))
}
```

Para usar `private_key_jwt` en lugar de un secreto de cliente, pase la clave privada. El paquete firma una nueva aserción de cliente para cada solicitud de token y de PAR (`iss` y `sub` son su ID de cliente, `aud` es el emisor, la duración es de 2 minutos y cada vez un nuevo `jti`):

```go
pemBytes, err := os.ReadFile("/run/secrets/keenauth-app-key.pem")
if err != nil {
	panic(err)
}
key, err := assertion.ParsePrivateKeyPEM(pemBytes) // import "github.com/keenauth/keenauth-go/assertion"
if err != nil {
	panic(err)
}
auth, err := bff.New(bff.Options{
	Issuer:     "https://acme.eu.keenauth.app",
	ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
	ClientAuth: bff.PrivateKeyJWT{Key: key, KeyID: "app-key-2026-11"},
	AppBaseURL: "https://app.acme.example",
	Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
})
```

Se admiten ES256 (P-256), EdDSA (Ed25519), RS256 y PS256 (RSA, de 2048 bits o más); el algoritmo depende de la clave, y `Algorithm: "PS256"` selecciona PSS para una clave RSA. `Key` puede ser cualquier `crypto.Signer`, por lo que también funciona una clave guardada en un KMS o HSM. Si firma en otro lugar, use `bff.PrivateKeyJWT{Assertion: f}`, donde `f` devuelve una nueva aserción firmada en cada llamada. Defina `Key` o `Assertion`, pero no ambos.

`AppBaseURL` es la dirección pública de su aplicación. El paquete construye a partir de ella el URI de redirección y todas las redirecciones, nunca a partir de los encabezados `Host` o `X-Forwarded-*` de la solicitud. Solo se acepta `http` sin cifrar para `localhost`.

## Rutas

`Middleware` sirve tres rutas y lee la sesión en cada una de las demás solicitudes. También puede montar las rutas por separado con `mux.Handle("/auth/", auth)`.

| Ruta | Qué hace |
|---|---|
| `GET /auth/login` | Inicia el inicio de sesión. Parámetros de consulta opcionales: `returnTo` (a dónde llega el usuario) y `organization` (iniciar sesión en una organización). |
| `GET /auth/callback` | Completa el inicio de sesión y establece la cookie de sesión. |
| `GET` o `POST /auth/logout` | Cierra la sesión en KeenAuth y devuelve al usuario a su aplicación. |

El inicio de sesión usa el flujo de código de autorización con PKCE (S256). La solicitud se envía al servidor (PAR) cuando su entorno lo ofrece. En el callback, el paquete comprueba `state`, `iss` y el token de ID, incluido su `nonce`. Cada inicio de sesión correcto crea una sesión nueva, y la anterior se descarta.

`returnTo` debe ser una ruta de su propia aplicación, como `/orders?tab=open`. Cualquier otro valor, como `//evil.example` o una URL completa, se ignora y el usuario llega a `/`.

## Proteger los manejadores

`auth.RequireAuth(handler)` deja pasar una solicitud solo si hay una sesión:

- una solicitud de página (`GET` o `HEAD`) sin sesión se redirige a `/auth/login` con `returnTo`;
- una solicitud a `/api/…`, o con otro método, recibe `401` con un documento de problema. Pase `bff.GuardOptions{API: true}` para responder `401` en cualquier ruta.

Para `POST`, `PUT`, `PATCH`, `DELETE` y otros métodos no seguros, el navegador debe indicar que la solicitud proviene de su aplicación: `Sec-Fetch-Site: same-origin`, o un encabezado `Origin` igual al origen de su aplicación. Las demás solicitudes reciben `403`. Esto protege contra la falsificación de solicitudes entre sitios, además de `SameSite=Lax`.

## Usar la sesión y llamar a su API

`bff.FromContext(r.Context())` devuelve:

- `Session`: `nil`, o el usuario que ha iniciado sesión. `Session.User` tiene los claims tipados del token de ID, como `Subject`, `Email`, `Name`, `OrgID` y `OrgRoles`; todos los claims están en `Session.User.Claims`. La sesión nunca contiene un token.
- `AccessToken`: un token de acceso válido para llamadas en el servidor, o `""`. No lo envíe al navegador.

Cuando el token de acceso expira en 60 segundos o menos, el paquete lo actualiza antes de que se ejecute su manejador. Cada actualización rota el token de actualización y establece la nueva cookie de sesión en la respuesta. Si el servidor rechaza el token de actualización, la sesión termina. Un error temporal del servidor deja la sesión sin cambios. Las solicitudes concurrentes en un mismo proceso comparten una sola actualización.

## Cambiar de organización

`auth.SwitchOrganizationPath("org_01j9…", "/dashboard")` devuelve un enlace a `/auth/login` que vuelve a iniciar sesión al usuario en esa organización. KeenAuth comprueba la pertenencia. Después del inicio de sesión, `Session.User.OrgID` contiene la organización.

## Cerrar sesión

Envíe un `POST` a `/auth/logout` desde un formulario. El paquete elimina la cookie de sesión (y la sesión almacenada, si usa un almacén), revoca el token de actualización y envía el navegador al endpoint de cierre de sesión de su entorno, que lo devuelve a su aplicación. Se rechaza una solicitud que el navegador marca como procedente de otro sitio. Los navegadores que no envían `Sec-Fetch-Site` solo pueden cerrar sesión con un `POST` desde sus propias páginas.

## Almacenamiento de sesiones

De forma predeterminada, toda la sesión está en la cookie, cifrada y autenticada con AES-256-GCM usando una clave derivada del secreto. Una cookie modificada o copiada se rechaza. La cookie se llama `__Host-keenauth_session.0` y se establece con `Secure`, `HttpOnly`, `SameSite=Lax` y `Path=/`. En `http://localhost` se omiten el prefijo `__Host-` y `Secure`. Las sesiones grandes se dividen en hasta tres cookies.

Para mantener los tokens en el servidor, defina `Options.Store` como un `bff.SessionStore` (`Get`, `Set`, `Delete`), por ejemplo respaldado por Redis. La cookie contendrá entonces solo un ID de sesión aleatorio y cifrado. Cada inicio de sesión crea un ID nuevo y elimina el registro anterior. El almacén contiene tokens, así que restrinja su acceso. `bff.MemoryStore` solo es para desarrollo y pruebas.

Una sesión dura como máximo 7 días (`SessionMaxAge`); actualizar no la prolonga. Para cambiar el secreto, indique primero el nuevo: `Secrets: []string{newSecret, oldSecret}`. Elimine el antiguo después de `SessionMaxAge`.

## Límites actuales

- Todavía no se gestionan los tokens vinculados a DPoP ni el cierre de sesión back-channel.
- Las actualizaciones solo se comparten dentro de un mismo proceso del servidor. Con varias instancias, la configuración de la aplicación `refresh_reuse_interval_s` (predeterminada 5 segundos) permite que una actualización paralela tenga éxito; si la establece en `0`, la segunda actualización termina la sesión.
