
# Proteja una API de Go

Esta guía de inicio rápido protege una API de Go `net/http` con tokens de acceso de KeenAuth. Añade el middleware `protect` del SDK de Go, exige un ámbito en una ruta y prueba la API sin conexión con el kit de pruebas del SDK. El middleware usa solo la biblioteca estándar y funciona con cualquier router que acepte un `http.Handler`.

La API tiene tres endpoints:

| Endpoint | Requiere |
|---|---|
| `GET /api/public` | nada |
| `GET /api/private` | un token de acceso válido para su API |
| `GET /api/messages` | un token de acceso válido con el ámbito `messages:read` |

## Antes de empezar

- Un entorno de KeenAuth. Su emisor tiene el formato `https://your-environment.eu.keenauth.app`.
- Una API registrada en el entorno ([Cree una API](/docs/api/management/apis/create)). Su `identifier`, por ejemplo `https://api.example.com`, es la audiencia de los tokens que acepta su API.
- El ámbito `messages:read` en esa API, concedido a la aplicación que la llama.
- Go 1.26 o posterior.

## 1. Instale el SDK

```sh
go get github.com/keenauth/keenauth-go
```

El SDK aún no está publicado. Mientras tanto, el ejemplo se compila con el código fuente del SDK mediante una directiva `replace` en `go.mod`.

## 2. Configure

La API lee su configuración solo desde variables de entorno:

| Variable | Obligatoria | Significado |
|---|---|---|
| `KEENAUTH_ISSUER` | sí | El emisor de su entorno |
| `KEENAUTH_AUDIENCE` | sí | El identificador de su API |
| `PUBLIC_BASE_URL` | sí | La URL que llaman los clientes (véase [Detrás de un proxy](#detrás-de-un-proxy)) |
| `KEENAUTH_WEBHOOK_SECRET` | recomendada | El secreto de firma `idp_whsec_` del endpoint de webhook que entrega `token.revoked` (dos, separados por comas, mientras lo rota). Sin él, la API nunca se entera de los tokens revocados |
| `ADDR` | no | Dirección de escucha, por defecto `:8080` |

## 3. Proteja sus rutas

`NewHandler` crea el middleware y las rutas. `m.Handler` exige un token válido; `m.Require` además exige ámbitos. El handler lee las claims verificadas con `protect.ClaimsFrom`.

```go
// NewHandler builds the API. The verifier is a parameter so that tests can pass the offline test kit's verifier;
// main passes a real one. The revocation cache is fed from the token.revoked webhook.
func NewHandler(v protect.AccessTokenVerifier, publicBaseURL string, revocation protect.RevocationChecker) (http.Handler, error) {
	m, err := protect.New(v, protect.Options{
		PublicBaseURL: publicBaseURL,
		Revocation:    revocation,
		OnError: func(r *http.Request, err error) {
			// The error never contains the token or claim values; never log the Authorization or DPoP header.
			log.Printf("rejected %s %s: %v", r.Method, r.URL.Path, err)
		},
	})
	if err != nil {
		return nil, err
	}

	mux := http.NewServeMux()
	// No token needed.
	mux.HandleFunc("GET /api/public", func(w http.ResponseWriter, _ *http.Request) {
		writeJSON(w, map[string]string{"message": "Hello from a public endpoint."})
	})
	// Any valid access token for this API.
	mux.Handle("GET /api/private", m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		claims, _ := protect.ClaimsFrom(r.Context())
		writeJSON(w, map[string]string{"message": "Hello from a private endpoint.", "sub": claims.Subject})
	})))
	// A valid access token that also carries the messages:read scope.
	mux.Handle("GET /api/messages", m.Require(protect.Requirement{Scopes: []string{"messages:read"}})(
		http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			writeJSON(w, map[string]any{"messages": []string{"Hello from a scoped endpoint."}})
		})))
	return mux, nil
}
```

`main` construye el verificador real a partir de la configuración. El verificador obtiene las claves de firma de su entorno la primera vez que ve un token.

```go
func main() {
	cfg, err := ConfigFromEnv(os.Getenv)
	if err != nil {
		log.Fatal(err)
	}
	// verify.New refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
	v, err := verify.New(verify.Config{Issuer: cfg.Issuer, Audience: cfg.Audience})
	if err != nil {
		log.Fatal(err)
	}
	revocation := protect.NewMemoryRevocationCache(0, nil)
	api, err := NewHandler(v, cfg.PublicBaseURL, revocation)
	if err != nil {
		log.Fatal(err)
	}
	h := api
	if len(cfg.WebhookSecrets) > 0 {
		wv, err := webhook.NewVerifier(cfg.WebhookSecrets) // the error never contains the secret
		if err != nil {
			log.Fatalf("KEENAUTH_WEBHOOK_SECRET: %v", err)
		}
		h = WithWebhook(api, NewWebhookHandler(wv, revocation, nil))
	} else {
		log.Print("KEENAUTH_WEBHOOK_SECRET is not set: token.revoked events are not received, so revoked tokens stay valid until they expire")
	}
	srv := &http.Server{Addr: cfg.Addr, Handler: h, ReadHeaderTimeout: 10 * time.Second}
	log.Printf("listening on %s", cfg.Addr)
	log.Fatal(srv.ListenAndServe())
}
```

## 4. Ejecute y llame a la API

```sh
export KEENAUTH_ISSUER=https://your-environment.eu.keenauth.app
export KEENAUTH_AUDIENCE=https://api.example.com
export PUBLIC_BASE_URL=https://api.example.com
go run .
```

Para una primera llamada, obtenga un token con el flujo client credentials de una aplicación máquina a máquina que tenga el ámbito `messages:read` en su API:

```sh
ACCESS_TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d resource=https://api.example.com \
  -d scope=messages:read \
  https://your-environment.eu.keenauth.app/oauth2/token | jq -r .access_token)

curl -i http://localhost:8080/api/messages -H "Authorization: Bearer $ACCESS_TOKEN"
```

## 5. Pruebe sin red

El paquete del kit de pruebas `keenauthtest` genera tokens firmados con una clave pública de prueba y construye un verificador que solo acepta esos tokens. Las pruebas pasan ese verificador a `NewHandler`, por lo que no necesitan red ni una cuenta de KeenAuth. Véase [Kit de pruebas sin conexión](/docs/sdk/testing).

```go
func newTestAPI(t *testing.T) (http.Handler, *protect.MemoryRevocationCache) {
	t.Helper()
	v, err := keenauthtest.NewVerifier(verify.Config{Audience: audience})
	if err != nil {
		t.Fatal(err)
	}
	cache := protect.NewMemoryRevocationCache(0, nil)
	h, err := NewHandler(v, baseURL, cache)
	if err != nil {
		t.Fatal(err)
	}
	return h, cache
}
```

```sh
go test ./...
```

Las pruebas del ejemplo comprueban que se acepta un token válido y que se rechazan, con los estados indicados abajo, un token ausente, caducado o con una audiencia incorrecta, un ámbito ausente, un token en la query de la URL y un token revocado. También envían un token vinculado con DPoP con una prueba para `PUBLIC_BASE_URL`. Además, envían entregas firmadas de `token.revoked`: una válida revoca el token, una falsificada, alterada o obsoleta recibe `400` y no revoca nada, y una repetida produce un solo efecto.

## Respuestas

| Solicitud | Estado | `code` en el cuerpo |
|---|---|---|
| Sin encabezado `Authorization` | `401` | `missing_token` |
| Token caducado, token para otra API, firma incorrecta, token revocado | `401` | `invalid_token` |
| Token sin el ámbito requerido | `403` | `insufficient_scope` |
| Token en la query de la URL (`?access_token=`) | `400` | `invalid_request` |

El cuerpo es `application/problem+json` y el encabezado `WWW-Authenticate` lleva el desafío estándar. La tabla completa está en [Protección de su API con middleware](/docs/sdk/protect-api#respuestas).

## Detrás de un proxy

Si los clientes usan DPoP, el middleware comprueba que cada prueba se hizo para la URL que llamó el cliente. Establezca `PUBLIC_BASE_URL` con la URL externa que llaman los clientes, por ejemplo `https://api.example.com`, también detrás de un proxy o balanceador de carga. Es obligatoria y debe usar `https` (`http` solo para `localhost`, `127.0.0.1` o `[::1]`); la aplicación se niega a arrancar sin ella. El middleware nunca confía en los encabezados `X-Forwarded-*`.

## Tokens revocados

Un token de acceso sigue siendo válido hasta que caduca, por lo que KeenAuth informa a su API cuando se revocan tokens. El ejemplo pasa una caché de revocación en memoria al middleware y sirve un webhook `token.revoked` en `POST /webhooks/keenauth` (`main.go, webhook.go`) que la alimenta.

1. En el panel, cree un endpoint de webhook con la URL `https://<your host>/webhooks/keenauth` que se suscriba a `token.revoked`, y exporte su secreto de firma como `KEENAUTH_WEBHOOK_SECRET`.
2. El handler verifica la firma con el SDK sobre el cuerpo sin procesar y responde `400` cuando no es válida o cuando la marca temporal se desvía más de cinco minutos. Asocia el selector del evento (`access_token`, `session`, `user`, `client`, `org`, `org_user`, `client_user`) con un marcador de revocación y responde `204`; a continuación, el middleware rechaza todos los tokens coincidentes emitidos antes de `not_before`.
3. KeenAuth entrega al menos una vez, así que el handler aplica cada `webhook-id` una sola vez. Si no se puede almacenar el marcador, responde `503` y KeenAuth vuelve a entregar el evento. Ignora otros tipos de evento y selectores que ningún token de acceso puede coincidir (`refresh_family`, `api`).

El handler registra el id del evento, el tipo de selector y el motivo, nunca el cuerpo, una firma ni el secreto. Cada proceso tiene su propia caché; con varias instancias, guarde los marcadores en un almacén compartido detrás del mismo método `Revoke`. Véase [Caché de revocación](/docs/sdk/protect-api#caché-de-revocación-tokenrevoked).

## Próximos pasos

- [Protección de su API con middleware](/docs/sdk/protect-api): requisitos de step-up, DPoP y todas las opciones.
- [Verificación de tokens](/docs/sdk/token-verification): el verificador que está bajo el middleware.
- [Kit de pruebas sin conexión](/docs/sdk/testing).
