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

1. Instale el SDK

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)
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.

// 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.

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

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:

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.

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
}
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.

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.

Próximos pasos