Aller au contenu principal

What is CORS? (Oh baby, don't hurt me)

· 12 minutes de lecture
Damien Buchet

Ok, ce sera la seule phrase en anglais, mais je ne pouvais pas passer à côté de ce titre !

Donc c'est quoi les CORS ? Comment ca fonctionne ? Pourquoi j'ai des erreurs ? Allons découvrir tout ca 💪

Une rapide explication ?

CORS, Cross-Origin Resource Sharing. C'est un mécanisme qui utilise les headers HTTP pour déterminer si une application est autorisée à accèder à des ressources d'une origine différente.

On va donc principalement rencontrer les CORS dans nos appels API depuis notre front-end vers notre back-end

XMLHttpRequest et l'API Fetch utilisent une politique same-origin, ce qui veut dire que, par défaut, vous pouvez uniquement accèder à des ressources de la même origine.

description du fonctionnement des cors, qui montre qu'une requete front depuis a-domain.com vers le back a-domain.com sera toujours autorisé, quand une requete depuis le front a-domain.com vers le back-end b-comain.com sera soumis aux CORS

A quoi ca sert concrètement ?

En bref, les CORS, c'est une sécurité HTTP (coté client) pour éviter que des domaines non autorisés viennent taper vos ressources.

Et vos "ressources" c'est quoi ? Et bien c'est votre API, vos images, vos vidéos, vos polices, vos css,...

Information sur l'atelier

Pour tous les exemples de l'atelier, je vais utiliser un petit back-end écrit en NodeJS + Express. Suivant vos langages et frameworks, la syntaxe va évidemment changer, mais le fonctionnement général des CORS restera le même.

Le front-end (que vous regardez en ce moment) est donc hébergé sur le domaine buchet.tech alors que l'api est hébergée sur europe-west1-buchet-tech.cloudfunctions.net, donc 2 domaines différents.

Je vous invite dès maintenant à ouvrir votre console développeur (CTRL+SHIFT+I ou F12) et d'aller sur l'onglet Network qui va nous servir tout au long de cet atelier. Je vous conseille également de faire un clic-droit sur les colonnes pour afficher la colonne "Method" qui va nous servir. Vous pouvez aussi dans ce panneau appuyer sur ECHAP pour avoir une vue splittée avec la console en bas.

Pour chaque exemple, vous pourrez voir le code du fetch (en front) et du endpoint correspondant (en back). Il vous suffira de cliquer sur le bouton "Lancer le call API" pour lancer la requête.

access-control-allow-origin

Comme dit plus haut, fetch est par défaut seulement autorisé sur la même origine. Donc que se passe-t-il si je fais une requête depuis ce domaine, vers mon API qui est sur un autre domaine ?

fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-1-1")

Vous devez voir dans l'onglet network une ligne en rouge avec un CORS Error et dans votre console, le message juste au dessus.

Important !

Un conseil très important avec les CORS, qui va nous servir tout le long de l'atelier, et qui d'une manière génerale sert tout le temps : Lisez les messages d'erreurs !

Alors, que nous dit ce message ? Il semberait qu'un header access-control-allow-origin ne soit pas présent dans le retour de la ressource (notre API donc). Et ca parle aussi d'un mode no-cors dont on parlera plus tard.

Comme dit en introduction, les CORS utilisent les headers HTTP. Depuis votre onglet Network, vous pouvez cliquer sur notre requête qui a echouée, puis ensuite dans la vue de droite qui vient de s'ouvrir sur l'onglet Headers. Cet onglet nous permet de tracer les headers de notre requête (Request Header), et ceux du retour de notre ressource (Response Header).

Dans la partie Response Header, il va y avoir plein d'informations, qui dépendent pour la plupart de votre serveur, votre backend, votre framework,... mais cela ressemble à quelque chose comme ceci

cache-control: private
content-encoding: gzip
content-type: text/html; charset=utf-8

Et comme on peut le voir, il n'y a aucune trace d'un access-control-allow-origin parmis tous les élements présent ici. Comme indiqué dans notre erreur !

Heureusement pour nous, on peut bien évidemment configurer notre back-end pour définir ce header, qui nous servira à autoriser des origines différentes.

app.get('/step-1-2', (_, res) => {
res.set('Access-Control-Allow-Origin', '*');
res.status(200).send("[GET] Hello World!")
});

Vous remarquez la ligne dans notre back-end qui va explicitement rajouter le header access-control-allow-origin à la réponse de notre API. Pour rester plus simple, on va utiliser ici le wildcard * qui permet d'autoriser tous les domaines. Ce n'est evidemment pas la configuration optimale si vous voulez protéger vos ressources, vous pouvez définir ici la liste de toutes les domaines qui seront autorisés à interroger votre API.

Regardons maintenant les headers de la réponse de ce call API

cache-control: private
content-encoding: gzip
content-type: text/html; charset=utf-8
access-control-allow-origin: *

🎉 Nous avons bien notre header access-control-allow-origin et la requête est passée sans problème !

Important !

La configuration des CORS se fait coté serveur. Le front n'a absolument aucun contrôle sur la configuration des CORS !

"Opaque response" ? Mode no-cors ?

Alors revenons à notre premier exemple qui a échoué. L'erreur nous parle de "réponse opaque" et de mode no-cors. Ca veut dire qu'il suffirait dans notre requête front de spécifier un mode no-cors pour s'affranchir des CORS ? Et bien essayons ! Nous allons de nouveau interroger l'endpoint sans access-control-allow-origin, avec un mode no-cors depuis le front.

fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-1-1", { 
mode: 'no-cors'
})

Alors, que se passe-t-il là ? La requête semble avoir fonctionné, mais je n'ai aucun retour.

En spécifiant le mode no-cors vous allez recevoir depuis votre back-end une réponse opaque (le fameux "Opaque response" de notre erreur du début). Et une réponse opaque, est simplement une réponse... où JS n'a pas accès au body (le corps / contenu) de la réponse !

En utilisant un mode no-cors vous n'aurez accès qu'aux headers de la réponse, jamais le body. Donc si vous n'avez besoin que de récupérer les headers, ce mode peut vous servir. Inutile de vous dire que ce n'arrive pas tous les jours.

Vous remarquerez que depuis l'onglet Network, vous avez bien accès à la réponse. Je vous avoue que je ne sais pas spécialement pourquoi la réponse est visible à cet endroit, mais dans tous les cas, votre JS n'y a pas accès.

danger

N'utilisez le mode no-cors que si vous savez ce que vous faites, et que vous n'avez besoin que des headers de la réponse !

no-cors n'est pas un mode magique qui vous permet de vous affranchir par miracle d'une sécurité HTTP 😉

Les requêtes "complexes"

Le protocole HTTP distingue des requêtes "simples" comme GET et POST sans headers additionnels. Donc si nous avons besoin de faire des GET et des POST "simples", tout devrait bien se dérouler.

Par contre comment ca se passe si j'ai besoin de faire un PUT, un DELETE ? Ou d'envoyer des headers supplémentaire dans ma requête ? Par exemple un Content-Type: 'application/json' que vous avez surement partout dans vos applications ?

Voyons ces 2 cas de figure, tout d'abord avec un GET et un header supplémentaire. On ne touche pas au back-end qui a donc toujours notre 'Access-Control-Allow-Origin' :

fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-2-1", {
method: 'get',
headers: {
'Content-Type': 'application/json'
}
})

Et avec un PUT

fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-2-1", {
method: 'put',
})

Alors mais quoi ? J'ai bien mon access-control-allow-origin qui est défini ! Pourquoi ca marche pas ?

On se rappelle du premier point important ? Lire les messages d'erreurs. Ok c'est le même que tout à l'heure et alors ? Et alors... BIEN LIRE les messages d'erreur

Il y a une information supplémentaire très importante dans ce message d'erreur (qui n'est donc pas le même que tout à l'heure 😉) Response to preflight request doesn't pass access control check

Preflight ? Access control check ? Qu'est ce que c'est encore que ca ?

Preflight

Regardons notre onglet Network dans la DevTools, vous allez voir que vous avez en fait 2 requêtes qui sont parties pour chaque fetch

step-2-1          GET          CORS Error        fetch
step-2-1 OPTIONS 200 preflight

Alors, pourquoi 2 call, et c'est quoi ce OPTIONS en même temps que mon GET ?

OPTIONS c'est une methode HTTP qui va décrire les options de communications de votre requête, et qui va donc servir à s'assurer que votre requete sera bien autorisée par le serveur, avant de l'effectuer. Et ce "check" est appelé preflight

Alors comment ca marche ce OPTIONS ? Et bien comme toutes les autres methodes HTTP, donc affichons les headers de cette requête

allow: GET,HEAD,PUT
content-encoding: gzip
content-type: text/html; charset=utf-8

Aucune trace access-control-allow-origin dans ces headers. Donc les preflight échoue. Donc votre fetch est rejeté.

Alors, comment régler ca ? Une fois encore ca sera du côté de votre back-end. OPTIONS est une methode HTTP, donc nous allons devoir définir une route du coté de notre API pour cette méthode. Dans laquelle nous allons définir notre access-control-allow-origin. Ainsi le preflight ne sera pas bloqué.

app.get('/step-3-1', (_, res) => {
res.set('Access-Control-Allow-Origin', '*');
res.status(200).send("[GET] Hello World!")
});

app.options('/step-3-1', (_, res) => {
res.set('Access-Control-Allow-Origin', '*');
res.status(200).send();
});

Et.... toujours une erreur. Inspectons les headers de la réponse

allow: GET,HEAD,PUT
content-encoding: gzip
content-type: text/html; charset=utf-8
access-control-allow-origin: *

Nous avons bien nos access-control-allow-origin. Donc l'erreur ne vient pas de là. Et vous le savez maintenant, on lit bien l'erreur : Request header field content-type is not allowed by access-control-allow-headers.

Request headers & Access-Control-*

Alors regardons maintenant les headers de notre requête ( et non pas de la réponse cette fois). Les informations peuvent encore une fois varier en fonction de votre navigateur, votre OS,... Mais nous avons quelque chose du genre :

accept: */*
accept-encoding: gzip, deflate, br
accept-language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7
access-control-request-headers: content-type
access-control-request-method: GET

Vous remarquez ici access-control-request-headers et access-control-request-method. Ces access-control-request-* sont envoyé par votre call api, et définissent quelles sont les "autorisations" que demandent votre requête.

Tous les access-control-request-* depuis les headers de la requête, doivent correspondre à un access-control-allow-* dans les headers de la réponse. Ces pairs request/allow sont plus communément abrégés en access-control-*

Et on voit bien dans les headers de la réponse de notre précédente requête, qu'il n'y a aucune trace de access-control-allow-headers et access-control-allow-method. donc le preflight a échoué.

Alors comment régler ca ? Et bien on retourne dans notre back-end et on autorise ces requêtes pour le GET, et évidemment également pour OPTIONS !

app.get('/step-4-1', (_, res) => {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Method', 'GET,OPTIONS');
res.set('Access-Control-Allow-Headers', 'Content-Type')
res.status(200).send("[GET] Hello World!")
});

app.options('/step-4-1', (_, res) => {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Method', 'GET,OPTIONS');
res.set('Access-Control-Allow-Headers', 'Content-Type')
res.status(200).send();
});

Et ca marche ! Regardons maintenant les headers de notre réponse

allow: GET,HEAD,PUT
content-encoding: gzip
content-type: text/html; charset=utf-8
access-control-allow-headers: content-type
access-control-allow-method: GET,OPTIONS
access-control-allow-origin: *

Nous avons bien nos deux access-control-allow-headers et access-control-allow-method dans la réponse, qui correspondent à nos deux access-control-request-headers et access-control-request-method de notre requête.

info

Vous remarquez également que notre access-control-allow-origin fait parti de ces access-control-*. Votre navigateur, pour chaque requête, va automatiquement envoyer access-control-request-origin, que vous pouvez voir dans les headers de la requête

Copions maintenant le même code pour notre PUT, et testons

fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-4-2", {
method: 'put'
})

Comme l'erreur le précise, et comme vous l'avez surement remarqué, nous essayons de faire un PUT sans l'avoir défini dans les access-control-allow-method (il n'y a que GET et OPTIONS) donc le preflight échoue !

Il suffira ici de changer Access-Control-Allow-Method pour que tout fonctionne !

res.set('Access-Control-Allow-Method', 'GET,PUT,OPTIONS');

Vous pouvez ainsi contrôler, endpoint par endpoint, quelles sont les origines, headers, et méthodes autorisées.

Est-ce que je peux rajouter des headers "custom" ?

Tout à fait, rajoutons un header custom dans notre exemple avec GET et observons la réponse

fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-4-1", {
method: 'get',
headers: {
'Content-Type': 'application/json',
'Custom-Param': 'myValue'
}
})

Comme vous pouvez le voir dans l'erreur, et si vous inspectez les headers de votre requête,

access-control-request-headers: content-type,custom-param

Vous avez désormais votre request-headers qui inclut votre custom-param. Vous devez donc, depuis votre back-end, autoriser les mêmes paramètres et changer votre Access-Control-Allow-Headers pour :

res.set('Access-Control-Allow-Headers', 'Content-Type, Custom-Param')
info

Vous pouvez définir plus de valeurs dans le Allow que dans le Request. C'est à dire que définir le Access-Control-Allow-Headers suivant est tout à fait valable

res.set('Access-Control-Allow-Headers', 'Content-Type, Custom-Param, Custom-Param2, Custom-Param3, Custom-Param4')

Il faudra cependant que toutes les valeurs de la Request soient présentes dans le Allow

Et pour les call coté serveur ?

Jusqu'à présent, on a vu les CORS pour des appels API depuis le front, mais est-ce que les CORS s'appliquent aussi quand je fais des call depuis mon back-end ? Pour la faire courte : Non

Pourquoi ? Parce que les CORS s'appliquent uniquement en Browser-to-Server et non en Server-to-Server. Il est également assez facile pour un serveur d'instaurer une "IP Rate Limit" qui va bloquer des serveurs distant qui font trop call, voire même les blacklister. Ce qui est plus compliqué à faire avec des front-end.

C'est le moment où vous vous dites :

Mais alors, si j'essaye de taper une API distante qui ne marche pas à cause des CORS et je m'en sors pas, il me suffit de faire un petit back-end qui va interroger cette API pour moi et jouer le rôle de proxy ?

Alors, oui, c'est possible. C'est même pratique lors de la phase de developpement, car la plupart des API n'autorisent pas localhost. Mais en production, ne le faites pas 😉 Et voila pourquoi :

Le temps de requête

Votre call va faire un "bond" sur votre proxy, qui va interroger l'API, qui va répondre à votre proxy, qui va vous répondre. Et pendant ce temps, c'est votre utilisateur qui patiente.

La sécurité !

Si vous avez une grosse fréquentation, tous vos utilisateurs vont passer par votre proxy. Donc déjà si vous n'avez pas un serveur proxy qui tient la charge, vous pouvez vous DDoS vous-même. Et il sera également très facile pour l'API distante de blacklister l'IP de votre serveur, rendant votre app inutilisable

En bref

info

Les CORS sont une sécurité HTTP pour empécher des domaines non autorisés de requêter des ressources

astuce

Les règles de sécurité des CORS sont définies par le back-end

attention

Tous les Access-Control-Request-* des headers de la requête doivent correspondre avec les Access-Control-Allow-* des headers de la réponse

danger

Utilisez un proxy uniquement pour le développement, jamais pour la production

Librairies pour gérer les CORS

Ressources externes