Todo CORS explicado de la A a la Z
Tags: webdev, cors, frontend, http
Published: 2023-01-02 19:19:40

## Definición de CORS

Antes de definir CORS se definirán algunos conceptos fundamentales para entender CORS.

### origen

Dada una URL, el origen es la primera parte que está compuesta por el esquema: el host y el puerto (si está presente).

> origen = esquema + host + puerto

Por ejemplo, en `https://localhost:8080/encuestas` el origen es `https://localhost:8080` y `/encuesta` es el path

### mismo-origen (same-origin)

Se refiere al caso en el que cliente hace una petición a un servidor y el servidor tiene el mismo origen que el cliente.

```
https://aurora.net/home -> https://aurora.net/api/pos?id=290
```

### origen-cruzado (cross-origen)

El cliente hace una petición a un servidor cuyo origen es distinto al del cliente.

```
https://localhost:8080/home -> https://aurora.net/api/pos?id=290
https://localhost:8080/home -> https://127.0.0.1:8080/api/pos?id=290
https://localhost:8080/home -> https://127.0.0.1:3000/api/pos?id=290
```

### Política de mismo-origen y CORS

Por defecto y por política de seguridad, los navegadores sólo permiten hacer peticiones HTTP al mismo origen, es decir al mismo dominio del de la página que hace la petición.

CORS (Cross-Origin Resource Sharing) es el mecanismo por el cual los navegadores permiten que una petición se haga desde un origen a otro distinto, origen-cruzado.

Para poder realizar una petición CORS el cliente no debe hacer nada, simplemente lanza la petición. El navegador hace la petición y es el servidor el que debe responder si acepta peticiones desde ese origen. Esto lo hace enviando la cabecera (header response) `Access-Control-Allow-Origin`. Esta cabecera puede tener un asterisco (`*`) o un origen específico. El `*` significa que el servidor acepta peticiones de cualquier origen.
El navegador revisa la respuesta del navegador. Si tiene la cabecera `Access-Control-Allow-Origin` y ésta tiene el valor `*` o el origen coincide con el origen del cliente, entonces el navegador acepta la respuesta. En caso contrario la descarta y generalmente muestra en consola un error descriptivo de CORS.

```
 1. navegador con origen X hace petición a origen Y
 2. servidor responde
 3. Si la respuesta del servidor incluye la cabecera `Access-Control-Allow-Origin` Entonces
 4.   Si (Access-Control-Allow-Origin == '*') o (Access-Control-Allow-Origin == X) Entonces
 5.     navegador acepta la respuesta
 6.   Si no
 7.     descarta respuesta y muestra error
 8.   Fin Si
 9. Si no
10.   descarta respuesta y muestra error
11. Fin Si
```

> `Access-Control-Allow-Origin` también puede tomar el valor `null` para orígenes desconocidos (como por ejemplo en el caso de acceder desde un fichero en vez de un sitio web). Esto es útil mientras se está en una fase de desarrollo o pruebas, de esta manera no hace falta poner '\*' que es menos restrictivo. `Access-Control-Allow-Origin: null`

## Preflight

Arriba se ha visto de manera genérica cómo se debe tratar una petición de origen-cruzado usando CORS. En realidad, antes de hacer una petición de origen-cruzado, los navegadores que implementan CORS harán una "meta" petición previa (preflight) a la petición real.

Un preflight es una petición que hace el navegador antes de efectuar una petición CORS si dicha petición cumple uno de estos criterios:

- Usa un método HTTP distinto de `GET`, `POST`, o `HEAD`
- Establece la cabecera `Content-Type` con un valor distinto de:
  - application/x-www-form-urlencoded
  - multipart/form-data
  - text/plain
- Establece cabeceras adicionales distintas de:
  - `Accept`
  - `Accept-Language`
  - `Content-Language`
- El objeto XMLHttpRequest contiene eventos upload

> A los métodos HTTP `GET`, `POST` y `HEAD` se los denomina métodos simples. Los métodos simples no necesitan ser listados en la cabecera `Control-Allow-Methods`.

El preflight es una petición con el método HTTP `OPTIONS`. Esta "meta" petición que hace el navegador sirve para que el servidor decida cómo responder al CORS. En el preflight el navegador envía las cabeceras:

- `Origin`
- `Access-Control-Request-Method` (admite sólo un método)
- `Access-Control-Request-Headers` (sólo si las cabeceras no son simples. Puede tener una lista de cabeceras separada por comas)

  El servidor comprueba si el origen y el método HTTP están en su lista de orígenes y métodos permitidos (también comprobará las cabeceras indicadas en `Access-Control-Request-Headers`, si es que lo hay). Si esta comprobación es favorable responde con un código de status en el rango `200`. La respuesta no debería llevar un body. Las cabeceras enviadas por el servidor son:

- `Access-Control-Allow-Origin`
- `Access-Control-Allow-Methods` (en este caso puede ser una lista de comandos separados por coma)
- `Access-Control-Allow-Headers`

Ejemplo de petición preflight:

```
OPTIONS /api/posts HTTP/1.1
User-Agent: Chrome
Host: 127.0.0.1:8080
Accept: */*
Origin: http://localhost:3000
Access-Control-Request-Method: GET
Access-Control-Request-Headers: Security-Level, App-Name
```

Ejemplo de respuesta preflight:

```
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http//localhost:3000
Access-Control-Allow-Methods: GET, DELETE
Access-Control-Allow-Headers: Security-Level, App-Name
```

Ejemplo de petición:

```
GET /api/posts HTTP/1.1
User-Agent: Chrome
Host: 127.0.0.1:8080
Accept: */*
Origin: http://localhost:3000
Security-Level: High
App-Name: Encuesta
```

El algoritmo anterior, ahora modificado con los preflight quedaría así:

```
 1. el cliente Javascript cuyo origen es X, hace una petición P al servidor con origen Y.
       P contiene el método HTTP M y un conjunto de cabeceras C
 2. Si M es un método HTTP simple y todas las cabeceras de C son simples Entonces
 3.   enviar la petición P al servidor
 4. Si no
 5.   preparar una petición preflight F con método HTTP OPTIONS y un conjunto de cabeceras C' con:
        Origin = X, Access-Control-Request-Method = M
 6.   Si en C hay cabeceras que no son simples Entonces
 7.     agregar a C' la cabecera Access-Control-Request-Headers con el listado de cabeceras no simples de C
 8.   Fin Si
 9.   enviar preflight F al servidor
10    Si servidor responde el preflight con status en el rango de 200 Entonces
11.     enviar la petición P al servidor
12.   Si no
13.     comprobar las cabeceras de respuesta Access-Control-Allow-Origin,
          Access-Control-Allow-Methods y Access-Control-Allow-Headers para determinar cuáles
          son las validaciones que han fallado e informar el error
14.   Fin Si
15. Fin Si
```

> Cache. El navegador puede guardar en caché los preflights y así evitar hacer una petición extra por cada petición del cliente que requeriría preflight. La caché guarda en sus entradas origen + path.

## Canvas e imágenes

En el caso de imágenes cuyo atributo `src` es un origen distinto al del cliente, el navegador carga el recurso y lo muestra pero no permite hacer manipulaciones con ella como `toBlob`, `toDataURL` y `getImageData`.
Para habilitar el CORS en el caso de imágenes, existe el atributo `crossOrigin` que puede tomar los valores `anonymous` o `user-credentials`. `user-credentials` es como el `withCredentials` de XMLHttpRequest, es decir que las credenciales de usuario (por ejemplo, cookies) se envían con la petición. Si no es necesario enviar las credenciales, es preferible enviar `anonymous`.

## Cookies y credenciales de usuario

Por defecto el navegador no envía las credenciales de usuario como las cookies en las peticiones de origen-cruzado. El navegador tampoco expone todos las cabeceras de respuesta que envía el servidor.
Por eso CORS tiene mecanismos para remediar esas restricciones: las cabeceras de respuesta `Access-Control-Allow-Credentials` y `Access-Control-Expose-Headers`.

Entonces para que el uso de cookies es necesario que el cliente indique:

```javascript
xhr.withCredentials = true;
document.cookie = someUniqueId;
```

El servidor debe responder al preflight y a la petición con las cabeceras
`Access-Control-Allow-Credentials` y `Access-Control-Allow-Origin`.

Cuando servidor envía la cabecera `Access-Control-Allow-Credentials` con el valor `true`, debe enviar la cabecera `Access-Control-Allow-Origin` con un origen específico; `Access-Control-Allow-Origin: *` es inválido porque aceptar cookies de cualquier origen puede generar problemas de seguridad.

Válido:

```
Access-Control-Allow-Origin: http://localhost:1111
Access-Control-Allow-Credentials: true
```

Inválido:

```
Access-Control-Allow-Origin: \*
Access-Control-Allow-Credentials: true
```

En el caso en el que el cliente tiene `xhr.withCredentials = true` y el servidor responde con `Access-Control-Allow-Origin: true`, el servidor puede establecer cookies en el cliente. Estas cookies serán enviadas por el navegador en las peticiones sucesivas, sin embargo, el client no podrá leer dichas cookies con Javascript.

## Cabeceras de respuesta

El objeto XMLHttpRequest expone dos métodos para leer las cabeceras de respuesta: `getResponseHeader` y `getAllResponseHeaders`. En una petición mismo-origen estos métodos devuelven todas las cabeceras enviadas por el servidor. Cuando se trata de una petición de origen-cruzado, sólo se pueden leer las cabeceras simples.

Usando la cabecera `Access-Control-Expose-Headers` el servidor puede especificar qué cabeceras puede leer el cliente.

```
Access-Control-Expose-Headers: X-Powered-By
```

> Cabeceras simples:
>
> - `Cache-Control`
> - `Content-Language`
> - `Content-Type`
> - `Expires`
> - `Last-Modified`
> - `Pragma`

## Resumen de cabeceras usadas en CORS

### Cabeceras de petición (enviadas por el navegador cliente)

- `Origin`: Cabecera que añade el navegador. Contiene el origen del cliente en la petición. Contraparte: `Access-Control-Allow-Origin`.
- `Access-Control-Request-Method`: Cabecera que añade el navegador en el preflight. Contiene el método HTTP que se hará el cliente en la petición real. Contraparte: `Access-Control-Allow-Methods`.
- `Access-Control-Request-Headers`: Cabecera que añade el navegador en el preflight. Contiene las cabeceras HTTP que enviará el cliente en la petición real. Contraparte: `Access-Control-Allow-Headers`.

### Cabeceras de respuesta (enviadas por el servidor)

- `Access-Control-Allow-Origin`: El valor `*` indica que se permiten todos los orígenes
  Un valor específico indica que el servidor sólo acepta peticiones de ese origen
  El valor `null` indica que sólo acepta peticiones de orígenes que no son un sitio web, por ejemplo un fichero (sólo recomendado para desarrollo y pruebas). Contraparte: `Origin`

- `Access-Control-Allow-Credentials`: El valor `true` indica que el servidor soporta el uso de credenciales de usuario tales como las cookies.

- `Access-Control-Allow-Methods`: Tiene una lista de métodos HTTP aceptados por el servidor.
  No es necesario incluir los métodos simples, aunque es buena práctica hacerlo.
  Esta cabecera sólo debe enviarse en respuesta a peticiones preflight. Contraparte: `Access-Control-Request-Methods`

- `Access-Control-Allow-Headers`: Tiene una lista de cabeceras HTTP aceptadas por el servidor.
  Esta cabecera sólo debe enviarse en respuesta a peticiones preflight. Contraparte:`Access-Control-Request-Headers`

- `Access-Control-Max-Age`: Su valor especifica el tiempo máximo en segundos que se recomienda mantener en caché un preflight, aunque el navegador puede tener su propio valor máximo.
  Esta cabecera sólo debe enviarse en respuesta a peticiones preflight.

- `Access-Control-Expose-Headers`: Contiene las cabeceras que pueden ser leídos por el navegador. Puede tener una lista de cabeceras separada por comas.
  Es opcional y no es necesario para que la petición CORS sea exitosa.