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.
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 ?
- Front-end
- Back-end
fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-1-1")
app.get('/step-1-1', (_, res) => {
res.status(200).send("[GET] Hello World!")
});
Vous devez voir dans l'onglet network une ligne en rouge avec un CORS Error
et dans votre console, le message juste au dessus.
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.
- Front-end
- Back-end
fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-1-2")
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 !
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.
- Front-end
- Back-end
fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-1-1", {
mode: 'no-cors'
})
app.get('/step-1-1', (_, res) => {
res.status(200).send("[GET] Hello World!")
});
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.
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'
:
- Front-end
- Back-end
fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-2-1", {
method: 'get',
headers: {
'Content-Type': 'application/json'
}
})
app.get('/step-2-1', (_, res) => {
res.set('Access-Control-Allow-Origin', '*');
res.status(200).send("[GET] Hello World!")
});
Et avec un PUT
- Front-end
- Back-end
fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-2-1", {
method: 'put',
})
app.put('/step-2-1', (_, res) => {
res.set('Access-Control-Allow-Origin', '*');
res.status(200).send("[PUT] Hello World!")
});
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é.
- Front-end
- Back-end
fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-3-1", {
method: 'get',
headers: {
'Content-Type': 'application/json'
}
})
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
!
- Front-end
- Back-end
fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-4-1", {
method: 'get',
headers: {
'Content-Type': 'application/json'
}
})
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.
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
- Front-end
- Back-end
fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-4-2", {
method: 'put'
})
app.put('/step-4-2', (_, 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("[PUT] Hello World!")
});
app.options('/step-4-2', (_, 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();
});
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
- Front-end
- Back-end
fetch("https://europe-west1-buchet-tech.cloudfunctions.net/cors/step-4-1", {
method: 'get',
headers: {
'Content-Type': 'application/json',
'Custom-Param': 'myValue'
}
})
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();
});
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')
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
Les CORS sont une sécurité HTTP pour empécher des domaines non autorisés de requêter des ressources
Les règles de sécurité des CORS sont définies par le back-end
Tous les Access-Control-Request-*
des headers de la requête doivent correspondre avec les Access-Control-Allow-*
des headers de la réponse
Utilisez un proxy uniquement pour le développement, jamais pour la production
Librairies pour gérer les CORS
- [NodeJS] https://github.com/expressjs/cors
- [Rails] https://github.com/cyu/rack-cors
- [Symfony] https://github.com/nelmio/NelmioCorsBundle
- [Laravel] https://github.com/fruitcake/laravel-cors
- [Python] https://github.com/corydolphin/flask-cors
- [Spring] https://spring.io/blog/2015/06/08/cors-support-in-spring-framework