17. Peticiones cross origin

Vamos a cambiar completamente de tema y actualizar nuestra aplicación para que admita solicitudes de origen cruzado (CORS) desde JavaScript.

Aprenderás:

  • Qué son las solicitudes de origen cruzado y por qué los navegadores web las previenen por defecto.
  • La diferencia entre solicitudes de origen cruzado simples y previas(preflight cross-origin requests).
  • Cómo utilizar encabezados de Control de Acceso para permitir o denegar solicitudes de origen cruzado específicas.
  • Acerca de las consideraciones de seguridad que debes tener en cuenta al configurar la configuración de CORS en tu aplicación.

17.1 Una Visión General de CORS

Antes de sumergirnos en cualquier código o comenzar a hablar específicamente sobre solicitudes de origen cruzado, tomémonos un momento para definir qué entendemos por el término “origen”.

Básicamente, si dos URL tienen el mismo esquema, host y puerto (si se especifica), se dice que comparten el mismo origen. Para ayudar a ilustrar esto, comparemos las siguientes URL:

URL AURL BSame Origin?Reason
https://foo.com/ahttp://foo.com/aNoDifferent scheme (http vs https)
http://foo.com/ahttp://www.foo.com/aNoDifferent host (foo.com vs www.foo.com)
http://foo.com/ahttp://foo.com:443/aNoDifferent port (no port vs 443)
http://foo.com/ahttp://foo.com/bYesOnly the path is different
http://foo.com/ahttp://foo.com/a?b=cYesOnly the query string is different
http://foo.com/a#bhttp://foo.com/a#cYesOnly the fragment is different

Comprender qué son los orígenes es importante porque todos los navegadores web implementan un mecanismo de seguridad conocido como la política de misma origen (same-origin policy). Hay algunas diferencias muy pequeñas en cómo los navegadores implementan esta política, pero en términos generales:

  • Una página web en un origen puede incrustar ciertos tipos de recursos de otro origen en su HTML, incluidas imágenes, archivos CSS y JavaScript. Por ejemplo, hacer esto en tu página web está bien:
<img src="http://anotherorigin.com/example.png" alt="example image">
  • Una página web en un origen puede enviar datos a un origen diferente. Por ejemplo, está bien que un formulario HTML en una página web envíe datos a un origen diferente.

  • Pero a una página web en un origen no se le permite recibir datos de un origen diferente.

Lo más importante aquí es el último punto: la política de misma origen previene que un sitio web (potencialmente malicioso) en otro origen pueda leer (posiblemente información confidencial) de tu sitio web.

Es importante enfatizar que el envío de datos entre orígenes no está impedido por la política de misma origen, a pesar de ser igualmente peligroso. De hecho, esto es lo que hace posible los ataques CSRF y por qué necesitamos tomar medidas adicionales para prevenirlos, como el uso de cookies SameSite y tokens CSRF.

Como desarrollador, es más probable que te encuentres con la política de misma origen cuando hagas solicitudes entre orígenes desde JavaScript en un navegador.

Por ejemplo, digamos que tienes una página web en https://foo.com que contiene algún código JavaScript en el front-end. Si este JavaScript intenta hacer una solicitud HTTP a https://bar.com/data.json (un origen diferente), entonces la solicitud será enviada y procesada por el servidor bar.com, pero el navegador web del usuario bloqueará la respuesta para que el código JavaScript de https://foo.com no pueda verla.

En general, la política de misma origen es un salvaguarda de seguridad extremadamente útil. Pero, aunque sea buena en el caso general, en ciertas circunstancias es posible que quieras relajarla.

Por ejemplo, si tienes una API en api.example.com y una aplicación JavaScript de front-end confiable que se ejecuta en www.example.com, probablemente querrás permitir solicitudes entre orígenes desde el dominio confiable www.example.com a tu API.

O tal vez tengas una API pública completamente abierta y quieras permitir solicitudes entre orígenes desde cualquier lugar para que sea fácil para otros desarrolladores integrarla con sus propios sitios web.

Afortunadamente, la mayoría de los navegadores web modernos te permiten permitir o denegar solicitudes específicas entre orígenes a tu API configurando los encabezados de Control de Acceso en las respuestas de tu API. Explicaremos exactamente cómo hacerlo y cómo funcionan estos encabezados en los próximos capítulos.

17.2 - OMITIDO -

17.3 Simple CORS Requests

Perfecto, para relajar la política de misma origen y permitir que JavaScript pueda leer las respuestas de nuestros puntos finales de API, podemos configurar nuestro servidor para que incluya el siguiente encabezado en todas las respuestas de la API:

Access-Control-Allow-Origin: *

El encabezado de respuesta Access-Control-Allow-Origin se utiliza para indicar a un navegador que está permitido compartir una respuesta con un origen diferente. En este caso, el valor del encabezado es el carácter comodín * , lo que significa que está permitido compartir la respuesta con cualquier otro origen. Ahora vamos a crear una pequeña función de middleware enableCORS() en nuestra aplicación de API que establezca este encabezado:

package main

// Importaciones

func (app *application) enableCORS(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        next.ServeHTTP(w, r)
    })
}

Y luego actualiza tu archivo cmd/api/routes.go para que este middleware se utilice en todas las rutas de la aplicación. Sería así:

package main

// Importaciones

func (app *application) routes() http.Handler {
    router := httprouter.New()
    router.NotFound = http.HandlerFunc(app.notFoundResponse)
    router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)

    router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
    router.HandlerFunc(http.MethodGet, "/v1/movies", app.requirePermission("movies:read", app.listMoviesHandler))
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.requirePermission("movies:write", app.createMovieHandler))
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.requirePermission("movies:read", app.showMovieHandler))
    router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.requirePermission("movies:write", app.updateMovieHandler))
    router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.requirePermission("movies:write", app.deleteMovieHandler))

    router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler)
    router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler)
    router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler)

    // Agrega el middleware enableCORS().
    return app.recoverPanic(app.enableCORS(app.rateLimit(app.authenticate(router))))
}

Es importante señalar aquí que el middleware enableCORS() está deliberadamente posicionado temprano en la cadena de middleware.

Si lo colocáramos después de nuestro limitador de velocidad, por ejemplo, cualquier solicitud entre orígenes que exceda el límite de velocidad no tendría el encabezado Access-Control-Allow-Origin configurado. Esto significa que serían bloqueados por el navegador web del cliente debido a la política de misma origen, en lugar de que el cliente reciba una respuesta 429 Too Many Requests como debería.

17.3 Restricting Origins

Usar un comodín para permitir solicitudes entre orígenes, como lo estamos haciendo en el código anterior, puede ser útil en ciertas circunstancias (como cuando tienes una API completamente pública sin controles de acceso). Pero más a menudo probablemente querrás restringir CORS a un conjunto mucho más pequeño de orígenes confiables.

Para hacer esto, necesitas incluir explícitamente los orígenes confiables en el encabezado Access-Control-Allow-Origin en lugar de usar un comodín. Por ejemplo, si solo deseas permitir CORS desde el origen https://www.example.com, podrías enviar el siguiente encabezado en tus respuestas:

Access-Control-Allow-Origin: https://www.example.com

Si solo tienes un origen fijo del que deseas permitir solicitudes, entonces hacer esto es bastante simple: simplemente puedes actualizar tu middleware enableCORS() para codificar de manera rígida el valor del origen necesario.

Pero si necesitas admitir múltiples orígenes confiables, o si deseas que el valor sea configurable en tiempo de ejecución, entonces las cosas se vuelven un poco más complejas.

Correcto. Si solo tienes un origen fijo del que deseas permitir solicitudes, simplemente puedes actualizar tu middleware enableCORS() para codificar de manera rígida el valor del origen necesario, como se mencionó anteriormente.

Sin embargo, si necesitas admitir múltiples orígenes confiables o deseas que el valor sea configurable en tiempo de ejecución, la implementación se vuelve un poco más compleja. En ese caso, necesitarás diseñar una solución que pueda manejar múltiples orígenes de manera dinámica, ya sea leyendo los orígenes desde una configuración externa o permitiendo que los usuarios los configuren a través de una interfaz. Esto puede requerir cambios en la estructura de tu aplicación y un enfoque más flexible en la gestión de los orígenes confiables.

Para evitar esta limitación, necesitarás actualizar tu middleware enableCORS() para verificar si el valor del encabezado Origin coincide con uno de tus orígenes confiables. Si coincide, entonces puedes reflejar (o eco) ese valor de vuelta en el encabezado de respuesta Access-Control-Allow-Origin. Esto garantizará que solo los orígenes confiables tengan permiso para acceder a tu API a través de solicitudes CORS.

Nota: La especificación de origen web sí permite múltiples valores separados por espacios en el encabezado Access-Control-Allow-Origin, pero desafortunadamente, ningún navegador web realmente lo admite.

17.3 Supporting multiple dynamic origins

Vamos a actualizar nuestra API para que las solicitudes entre orígenes estén restringidas a una lista de orígenes confiables, configurables en tiempo de ejecución. Lo primero que haremos es agregar un nuevo flag -cors-trusted-origins a nuestra aplicación de API, que podemos usar para especificar la lista de orígenes confiables en tiempo de ejecución. Configuraremos esto de manera que los orígenes deben estar separados por un carácter de espacio, como se muestra a continuación:

$ go run ./cmd/api -cors-trusted-origins=&quot;https://www.example.com https://staging.example.com&quot;

Para procesar este flag de línea de comandos, podemos combinar las funciones flags.Func() y strings.Fields() para dividir los valores de origen en una slice []string lista para su uso.

Si estás siguiendo estos pasos, abre tu archivo cmd/api/main.go y agrega el siguiente código:

package main

import (
	"context"
	"database/sql"
	"flag"
	"log"
	"os"
	"strings"
	"sync"
	"time"

	"greenlight.alexedwards.net/internal/data"
	"greenlight.alexedwards.net/internal/mailer"

	_ "github.com/lib/pq"
)

const version = "1.0.0"

type config struct {
	port int
	env  string
	db   struct {
		dsn          string
		maxOpenConns int
		maxIdleConns int
		maxIdleTime  time.Duration
	}
	limiter struct {
		enabled bool
		rps     float64
		burst   int
	}
	smtp struct {
		host     string
		port     int
		username string
		password string
		sender   string
	}
	// Add a cors struct and trustedOrigins field with the type []string.
	cors struct {
		trustedOrigins []string
	}
}

func main() {
	var cfg config

	flag.IntVar(&cfg.port, "port", 4000, "API server port")
	flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")
	flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv("GREENLIGHT_DB_DSN"), "PostgreSQL DSN")
	flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections")
	flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections")
	flag.DurationVar(&cfg.db.maxIdleTime, "db-max-idle-time", 15*time.Minute, "PostgreSQL max connection idle time")
	flag.BoolVar(&cfg.limiter.enabled, "limiter-enabled", true, "Enable rate limiter")
	flag.Float64Var(&cfg.limiter.rps, "limiter-rps", 2, "Rate limiter maximum requests per second")
	flag.IntVar(&cfg.limiter.burst, "limiter-burst", 4, "Rate limiter maximum burst")
	flag.StringVar(&cfg.smtp.host, "smtp-host", "sandbox.smtp.mailtrap.io", "SMTP host")
	flag.IntVar(&cfg.smtp.port, "smtp-port", 25, "SMTP port")
	flag.StringVar(&cfg.smtp.username, "smtp-username", "a7420fc0883489", "SMTP username")
	flag.StringVar(&cfg.smtp.password, "smtp-password", "e75ffd0a3aa5ec", "SMTP password")
	flag.StringVar(&cfg.smtp.sender, "smtp-sender", "Greenlight <no-reply@greenlight.alexedwards.net>", "SMTP sender")

	// Use the flag.Func() function to process the -cors-trusted-origins command line
	// flag. In this we use the strings.Fields() function to split the flag value into a
	// slice based on whitespace characters and assign it to our config struct.
	// Importantly, if the -cors-trusted-origins flag is not present, contains the empty
	// string, or contains only whitespace, then strings.Fields() will return an empty
	// []string slice.
	flag.Func("cors-trusted-origins", "Trusted CORS origins (space separated)", func(val string) error {
		cfg.cors.trustedOrigins = strings.Fields(val)
		return nil
	})

	flag.Parse()
}

Nota: Si deseas obtener más información sobre la funcionalidad de flag.Func() y algunas de las formas en que puedes usarlo, he escrito una publicación de blog mucho más detallada al respecto aquí.

Una vez hecho esto, el siguiente paso es actualizar nuestro middleware enableCORS(). Específicamente, queremos que el middleware verifique si el valor del encabezado Origin de la solicitud es una coincidencia exacta y sensible a mayúsculas y minúsculas con uno de nuestros orígenes confiables. Si hay una coincidencia, entonces debemos establecer un encabezado de respuesta Access-Control-Allow-Origin que refleje (o haga eco) el valor del encabezado Origin de la solicitud.

De lo contrario, debemos permitir que la solicitud continúe como de costumbre sin establecer un encabezado de respuesta Access-Control-Allow-Origin. A su vez, eso significa que cualquier respuesta entre orígenes será bloqueada por un navegador web, tal como lo estaba originalmente.

Un efecto secundario de esto es que la respuesta será diferente dependiendo del origen desde el que provenga la solicitud. Específicamente, el valor del encabezado Access-Control-Allow-Origin puede ser diferente en la respuesta, o incluso es posible que ni siquiera se incluya.

Por lo tanto, debido a esto, debemos asegurarnos de siempre establecer un encabezado de respuesta Vary: Origin para advertir a cualquier caché que la respuesta puede ser diferente. Esto es realmente importante, y puede ser la causa de errores sutiles como este si olvidas hacerlo.

Como regla general: Si tu código toma una decisión sobre qué devolver basándose en el contenido de un encabezado de solicitud, debes incluir ese nombre de encabezado en tu encabezado de respuesta Vary, incluso si la solicitud no incluyó ese encabezado.

De acuerdo, actualicemos nuestro middleware enableCORS() de acuerdo con la lógica anterior, de la siguiente manera:

package main

import (
	"net/http"
)

func (app *application) enableCORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Add the "Vary: Origin" header.
		w.Header().Add("Vary", "Origin")
		// Get the value of the request's Origin header.
		origin := r.Header.Get("Origin")
		// Only run this if there's an Origin request header present.
		if origin != "" {
			// Loop through the list of trusted origins, checking to see if the request
			// origin exactly matches one of them. If there are no trusted origins, then
			// the loop won't be iterated.
			for i := range app.config.cors.trustedOrigins {
				if origin == app.config.cors.trustedOrigins[i] {
					// If there is a match, then set a "Access-Control-Allow-Origin"
					// response header with the request origin as the value and break
					// out of the loop.
					w.Header().Set("Access-Control-Allow-Origin", origin)
					break
				}
			}
		}
		// Call the next handler in the chain.
		next.ServeHTTP(w, r)
	})
}

Y con esos cambios completados, ahora estamos listos para probarlo nuevamente. Reinicia tu API, pasando http://localhost:9000 y http://localhost:9001 como orígenes confiables de la siguiente manera:

$ go run ./cmd/api -cors-trusted-origins="http://localhost:9000 http://localhost:9001"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development

17.3 Información adicional

17.3 Coincidencias parciales de origen

Si tienes muchos orígenes confiables que deseas admitir, es posible que te sientas tentado a verificar una coincidencia parcial en el origen para ver si “comienza con” o “termina con” un valor específico, o coincide con una expresión regular. Si haces esto, debes tener mucho cuidado para evitar coincidencias no deseadas.

Como ejemplo simple, si http://example.com y http://www.example.com son tus orígenes confiables, tu primera idea podría ser verificar que el encabezado de origen de la solicitud termina con example.com. Esto sería una mala idea, ya que un atacante podría registrar el nombre de dominio attackerexample.com y cualquier solicitud desde ese origen pasaría tu verificación.

Este es solo un ejemplo simple, y los siguientes artículos de blog discuten algunas de las otras vulnerabilidades que pueden surgir al usar coincidencias parciales o verificaciones de expresiones regulares:

En general, es mejor verificar el encabezado de solicitud “Origin” contra una lista de safelist explícita de orígenes confiables de longitud completa, como hemos hecho en este capítulo.

17.3 The null origin

Es importante nunca incluir el valor “null” como un origen confiable en tu lista de orígenes seguros. Esto se debe a que el encabezado de solicitud “Origin: null” puede ser falsificado por un atacante al enviar una solicitud desde un iframe en un sandbox.

17.3 Authentication and CORS

Si tu endpoint de API requiere credenciales (cookies o autenticación básica HTTP), también debes establecer un encabezado “Access-Control-Allow-Credentials: true” en tus respuestas. Si no estableces este encabezado, entonces el navegador web evitará que se lean las respuestas de origen cruzado con credenciales mediante JavaScript.

Es importante destacar que nunca debes usar el encabezado de origen cruzado comodín “Access-Control-Allow-Origin: *” en conjunto con “Access-Control-Allow-Credentials: true”, ya que esto permitiría que cualquier sitio web realice una solicitud de origen cruzado con credenciales a tu API.

Además, es importante mencionar que si deseas que se envíen credenciales con una solicitud de origen cruzado, deberás especificarlo explícitamente en tu JavaScript. Por ejemplo, con fetch(), deberías establecer el valor de credenciales de la solicitud como ‘include’. De la siguiente manera:

fetch("https://api.example.com", {credentials: 'include'}).then( ... );

O si estás utilizando XMLHTTPRequest, deberías establecer la propiedad withCredentials en true. Por ejemplo:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com');
xhr.withCredentials = true;
xhr.send(null);

17.4 Preflight CORS Requests

La solicitud de origen cruzado que realizamos desde JavaScript en el capítulo anterior se conoce como una solicitud de origen cruzado simple. En términos generales, las solicitudes de origen cruzado se clasifican como ‘simples’ cuando se cumplen todas las siguientes condiciones:

  • El método HTTP de la solicitud es uno de los tres métodos CORS seguros: HEAD, GET o POST.
  • Las cabeceras de la solicitud son todas cabeceras prohibidas o una de las cuatro cabeceras CORS seguras:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
  • El valor para la cabecera Content-Type (si está establecido) es uno de:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

Cuando una solicitud de origen cruzado no cumple estas condiciones, entonces el navegador web activará una solicitud de ‘preflight’ inicial antes de la solicitud real. El propósito de esta solicitud de preflight es determinar si se permitirá o no la solicitud de origen cruzado real.

17.4 Demonstrating a preflight request

Para ayudar a demostrar cómo funcionan las solicitudes de preflight y qué necesitamos hacer para tratar con ellas, crearemos otra página de ejemplo en el directorio cmd/examples/cors/. Configuraremos esta página para que realice una solicitud a nuestro endpoint POST /v1/tokens/authentication. Al llamar a este endpoint incluiremos una dirección de correo electrónico y una contraseña en el cuerpo de la solicitud JSON, junto con una cabecera Content-Type: application/json. Y dado que la cabecera Content-Type: application/json no está permitida en una solicitud de origen cruzado ‘simple’, esto debería activar una solicitud de preflight a nuestra API.

Continúa y crea un nuevo archivo en cmd/examples/cors/preflight/main.go:

$ mkdir -p cmd/examples/cors/preflight
$ touch cmd/examples/cors/preflight/main.go

Y agrega el siguiente código, que sigue un patrón muy similar al que usamos hace un par de capítulos:

package main

import (
	"flag"
	"log"
	"net/http"
)

// Define a string constant containing the HTML for the webpage. This consists of a <h1>
// header tag, and some JavaScript which calls our POST /v1/tokens/authentication
// endpoint and writes the response body to inside the <div id="output"></div> tag.
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>Preflight CORS</h1>
<div id="output"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
fetch("http://localhost:4000/v1/tokens/authentication", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'alice@example.com',
password: 'pa55word'
})
}).then(
function (response) {
response.text().then(function (text) {
document.getElementById("output").innerHTML = text;
});
},
function(err) {
document.getElementById("output").innerHTML = err;
}
);
});
</script>
</body>
</html>`

func main() {
	addr := flag.String("addr", ":9000", "Server address")
	flag.Parse()
	log.Printf("starting server on %s", *addr)
	err := http.ListenAndServe(*addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(html))
	}))
	log.Fatal(err)
}

Si estás siguiendo, adelante y ejecuta esta aplicación.

Luego, abre una segunda ventana de terminal y inicia nuestra aplicación API regular al mismo tiempo con http://localhost:9000 como origen de confianza:

$ go run ./cmd/api -cors-trusted-origins="http://localhost:9000"

Una vez que ambos estén en ejecución, abre tu navegador web y navega a http://localhost:9000. Si observas el registro de la consola en tus herramientas para desarrolladores, deberías ver un mensaje similar a este:

Solicitud de Origen Cruzado Bloqueada: La Política de Mismo Origen prohíbe la lectura del recurso remo> to en http://localhost:4000/v1/tokens/authentication. (Motivo: el encabezado ‘content-type’ no está permitido según el encabezado ‘Access-Control-Allow-Headers’ de la respuesta de preflight de CORS).

Podemos ver que hay dos solicitudes aquí marcadas como ‘bloqueadas’ por el navegador:

  1. Una solicitud OPTIONS /v1/tokens/authentication (esta es la solicitud de preflight).
  2. Una solicitud POST /v1/tokens/authentication (esta es la solicitud ‘real’).

Lo interesante aquí son las cabeceras de la solicitud de preflight. Pueden verse ligeramente diferentes dependiendo del navegador que estés utilizando, pero en general deberían lucir algo así:

Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.5
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: POST
Cache-Control: no-cache
Connection: keep-alive
Host: localhost:4000
Origin: http://localhost:9000
Pragma: no-cache
Referer: http://localhost:9000/
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0

Aquí hay tres cabeceras relevantes para CORS:

  • Origin: Como vimos anteriormente, esto le permite a nuestra API saber desde qué origen proviene la solicitud de preflight.
  • Access-Control-Request-Method: Esto le permite a nuestra API saber qué método HTTP se utilizará para la solicitud real (en este caso, podemos ver que la solicitud real será un POST).
  • Access-Control-Request-Headers: Esto le permite a nuestra API saber qué cabeceras HTTP se enviarán con la solicitud real (en este caso, podemos ver que la solicitud real incluirá una cabecera content-type).

Es importante tener en cuenta que Access-Control-Request-Headers no enumerará todas las cabeceras que usará la solicitud real. Solo se listarán las cabeceras que no son seguras para CORS o están prohibidas. Si no hay tales cabeceras, entonces Access-Control-Request-Headers puede omitirse por completo de la solicitud de preflight.

17.4 Responder a las solicitudes de preflight

Para responder a una solicitud de preflight, lo primero que necesitamos hacer es identificar que se trata de una solicitud de preflight, en lugar de una solicitud OPTIONS regular (posiblemente incluso de origen cruzado). Para hacerlo, podemos aprovechar el hecho de que las solicitudes de preflight siempre tienen tres componentes: el método HTTP OPTIONS, un encabezado Origin y un encabezado Access-Control-Request-Method. Si falta alguno de estos componentes, sabemos que no es una solicitud de preflight.

Una vez que identificamos que es una solicitud de preflight, necesitamos enviar una respuesta 200 OK con algunos encabezados especiales para informar al navegador si está bien o no que la solicitud real continúe. Estos son:

  • Un encabezado de respuesta Access-Control-Allow-Origin, que refleja el valor del encabezado Origin de la solicitud de preflight (como en el capítulo anterior).
  • Un encabezado Access-Control-Allow-Methods que enumera los métodos HTTP que se pueden usar en solicitudes reales de origen cruzado a la URL.
  • Un encabezado Access-Control-Allow-Headers que enumera los encabezados de solicitud que se pueden incluir en solicitudes reales de origen cruzado a la URL.

En nuestro caso, podríamos establecer los siguientes encabezados de respuesta para permitir solicitudes de origen cruzado para todos nuestros puntos finales:

Access-Control-Allow-Origin: &lt;origen confiable reflejado&gt;
Access-Control-Allow-Methods: OPTIONS, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type

Importante: Al responder a una solicitud de preflight, no es necesario incluir los métodos CORS seguros HEAD, GET o POST en el encabezado Access-Control-Allow-Methods. Del mismo modo, no es necesario incluir encabezados prohibidos o CORS seguros en Access-Control-Allow-Headers.

Cuando el navegador web recibe estos encabezados, compara los valores con el método y los encabezados (sin distinción entre mayúsculas y minúsculas) que desea utilizar en la solicitud real. Si el método o alguno de los encabezados no están permitidos, entonces el navegador bloqueará la solicitud real.

17.4 Actualizando nuestro middleware

Vamos a poner esto en acción y actualizar nuestro middleware enableCORS() para que intercepte y responda a cualquier solicitud de preflight. Específicamente, queremos:

  1. Establecer un encabezado Vary: Access-Control-Request-Method en todas las respuestas, ya que la respuesta será diferente dependiendo de si este encabezado existe o no en la solicitud.
  2. Verificar si la solicitud es una solicitud de preflight de origen cruzado o no. Si no lo es, entonces debemos permitir que la solicitud continúe como de costumbre.
  3. De lo contrario, si es una solicitud de preflight de origen cruzado, debemos agregar los encabezados Access-Control-Allow-Method y Access-Control-Allow-Headers como se describe arriba.

Adelante, actualiza el archivo cmd/api/middleware.go de la siguiente manera:

package main

...

func (app *application) enableCORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Add the "Vary: Origin" header.
		w.Header().Add("Vary", "Origin")
		// Add the "Vary: Access-Control-Request-Method" header.
		w.Header().Add("Vary", "Access-Control-Request-Method")

		origin := r.Header.Get("Origin")
		if origin != "" {
			for i := range app.config.cors.trustedOrigins {
				if origin == app.config.cors.trustedOrigins[i] {
					w.Header().Set("Access-Control-Allow-Origin", origin)
					// Check if the request has the HTTP method OPTIONS and contains the
					// "Access-Control-Request-Method" header. If it does, then we treat
					// it as a preflight request.
					if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
						// Set the necessary preflight response headers, as discussed
						// previously.
						w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, PUT, PATCH, DELETE")
						w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
						// Write the headers along with a 200 OK status and return from
						// the middleware with no further action.
						w.WriteHeader(http.StatusOK)
						return
					}
					break
				}
			}
		}

		next.ServeHTTP(w, r)
	})
}

Hay un par de cosas adicionales para señalar aquí:

  • Cuando respondemos a una solicitud de preflight, deliberadamente enviamos el código de estado HTTP 200 OK en lugar del 204 No Content, aunque no haya cuerpo de respuesta. Esto se debe a que ciertas versiones de navegadores pueden no admitir respuestas 204 No Content y bloquear posteriormente la solicitud real.
  • Si permites el encabezado Authorization en las solicitudes de origen cruzado, como lo estamos haciendo en el código anterior, es importante no establecer el encabezado wildcard Access-Control-Allow-Origin: * o reflejar el encabezado Origin sin verificarlo contra una lista de orígenes confiables. De lo contrario, esto dejaría tu servicio vulnerable a un ataque de fuerza bruta distribuido contra cualquier credencial de autenticación que se pase en ese encabezado. Bien, probemos esto. Reinicia tu API, nuevamente estableciendo http://localhost:9000 como un origen de confianza de la siguiente manera:
$ go run ./cmd/api -cors-trusted-origins=&quot;http://localhost:9000&quot;
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;database connection pool established&quot;
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;starting server&quot; addr=:4000 env=development

Luego abre http://localhost:9000 en tu navegador nuevamente. Esta vez deberías ver que la solicitud cross-origin fetch() a POST /v1/tokens/authentication tiene éxito, y ahora recibes un token de autenticación en la respuesta.

Nota: Si observas los detalles de la solicitud de preflight, deberías ver que nuestros nuevos encabezados CORS se han establecido en la respuesta de preflight, como se muestra con la flecha roja en la captura de pantalla anterior.

17.4 Informacion adicional

17.4 Caching preflight responses

Si lo deseas, también puedes agregar un encabezado Access-Control-Max-Age a tus respuestas de preflight. Esto indica el número de segundos que la información proporcionada por los encabezados Access-Control-Allow-Methods y Access-Control-Allow-Headers puede ser almacenada en caché por el navegador.

Por ejemplo, para permitir que los valores se almacenen en caché durante 60 segundos, puedes establecer el siguiente encabezado en tu respuesta de preflight:

Access-Control-Max-Age: 60

Si no estableces un encabezado Access-Control-Max-Age, las versiones actuales de Chrome/Chromium y Firefox usarán por defecto un tiempo de caché para estas respuestas de preflight de 5 segundos. Las versiones más antiguas u otros navegadores pueden tener diferentes valores predeterminados, o incluso no almacenar en caché los valores.

Establecer una duración larga de Access-Control-Max-Age puede parecer una forma atractiva de reducir las solicitudes a tu API, ¡y lo es! Pero también necesitas tener cuidado. No todos los navegadores proporcionan una forma de borrar la caché de preflight, por lo que si envías los encabezados incorrectos, el usuario quedará atrapado con ellos hasta que la caché expire.

Si deseas desactivar por completo la caché, puedes establecer el valor en -1:

Access-Control-Max-Age: -1

También es importante tener en cuenta que los navegadores pueden imponer un límite máximo en cuánto tiempo pueden ser almacenados en caché los encabezados. La documentación de MDN dice:

  • Firefox limita esto a 24 horas (86400 segundos).
  • Chromium (antes de la v76) lo limita a 10 minutos (600 segundos).
  • Chromium (a partir de la v76) lo limita a 2 horas (7200 segundos).

17.4 Preflight wildcards

Si tienes una API compleja o que cambia rápidamente, puede ser incómodo mantener una lista de métodos y encabezados predefinida en la respuesta de preflight. Podrías pensar: “Solo quiero permitir todos los métodos y encabezados HTTP para las solicitudes de origen cruzado”.

En este caso, tanto los encabezados Access-Control-Allow-Methods como Access-Control-Allow-Headers te permiten usar un carácter comodín * de la siguiente manera:

Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: *

Pero el uso de estos comodines conlleva algunas advertencias importantes:

  • Los comodines en estos encabezados solo son compatibles actualmente con el 74% de los navegadores. Cualquier navegador que no los admita bloqueará la solicitud de preflight.
  • El encabezado Authorization no puede ser sustituido por un comodín. En su lugar, deberás incluirlo explícitamente en el encabezado como Access-Control-Allow-Headers: Authorization, *.
  • Los comodines no son compatibles con las solicitudes con credenciales (aquellas con cookies o autenticación básica de HTTP). Para estas solicitudes, el carácter * se tratará como la cadena literal ”*”, en lugar de como un comodín.

Post Relacionados