Activer CORS et sécuriser les jetons d’accès avec HTTPOnly
Cet article explore la méthode d’activation du partage de ressources entre origines (CORS) tout en utilisant des cookies HTTPOnly pour renforcer la sécurité de nos jetons d’accès.
De nos jours, il est fréquent que les serveurs backend et les applications frontend soient hébergés sur des domaines distincts. Pour permettre une communication fluide entre ces entités, il est crucial que le serveur active CORS, facilitant ainsi les échanges sur les navigateurs web.
En outre, l’authentification sans état, privilégiée pour son évolutivité, implique de stocker les jetons d’authentification côté client, et non côté serveur comme dans le cas des sessions. Pour des raisons de sécurité accrues, il est vivement recommandé de conserver ces jetons dans des cookies HTTPOnly.
Pourquoi les requêtes d’origine croisée sont-elles bloquées ?
Imaginons que votre application frontend soit disponible à l’adresse https://app.toptips.fr.com. Un script exécuté sur cette page ne peut, par défaut, solliciter des ressources que de la même origine.
Toute tentative d’envoyer une requête vers un domaine différent, tel que https://api.toptips.fr.com, ou vers un autre port, comme https://app.toptips.fr.com:3000, ou encore via un autre protocole, tel que http://app.toptips.fr.com, entraînera le blocage de la requête par le navigateur.
Cependant, il est important de noter qu’une requête identique émise via un outil comme curl, ou tout autre client de serveur, ne subit pas ce blocage. Cela est dû à un mécanisme de sécurité mis en place pour prémunir les utilisateurs contre des attaques telles que la falsification de requêtes intersites (CSRF).
Prenons un exemple concret : un utilisateur est connecté à son compte PayPal dans son navigateur. Si une requête d’origine croisée pouvait être envoyée à paypal.com depuis un script exécuté sur un site malveillant (par exemple, malware.com), sans aucune restriction CORS, l’attaquant pourrait aisément transférer des fonds depuis le compte de l’utilisateur vers le sien.
Un attaquant pourrait diffuser un lien malicieux, sous la forme d’une URL raccourcie, dissimulant ainsi la vraie URL (par exemple, https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account). L’utilisateur cliquant sur ce lien exécuterait un script qui effectuerait le transfert sans son consentement.
C’est précisément pour éviter de telles situations que les navigateurs bloquent par défaut les requêtes inter-origines.
CORS : Qu’est-ce que c’est ?
Le CORS (Cross-Origin Resource Sharing) est un mécanisme de sécurité basé sur des en-têtes HTTP. Le serveur l’utilise pour signaler au navigateur que les requêtes inter-origines peuvent être autorisées depuis des domaines considérés comme fiables.
L’activation de CORS par le serveur permet d’éviter le blocage des requêtes inter-origines par les navigateurs.
Fonctionnement du CORS
Dans la configuration CORS du serveur, les domaines de confiance sont définis. Lorsqu’une requête est envoyée, la réponse du serveur indique au navigateur si le domaine demandeur est autorisé ou non, via un en-tête spécifique.
On distingue deux types de requêtes CORS :
- La requête simple
- La requête de pré-vérification
La requête simple :
- Le navigateur envoie la requête vers un domaine différent, en y incluant l’en-tête « origin » (par exemple, https://app.toptips.fr.com).
- Le serveur répond avec les méthodes HTTP autorisées et les origines approuvées.
- Le navigateur compare alors l’en-tête « origin » envoyé (https://app.toptips.fr.com) à la valeur de l’en-tête « access-control-allow-origin » reçue. Si les deux correspondent, ou si la valeur reçue est un caractère générique, la requête est autorisée.
Si les valeurs ne correspondent pas, une erreur CORS est générée.
- Requête de pré-vérification :
- Si la requête inter-origine possède des paramètres spécifiques, comme des méthodes HTTP (PUT, DELETE), des en-têtes personnalisés, ou un type de contenu non standard, le navigateur enverra une requête OPTIONS préalable afin de vérifier si la requête originale peut être effectuée en toute sécurité.
Si la réponse à cette requête OPTIONS (code 204, signifiant « aucun contenu ») contient des paramètres autorisant la requête originale, celle-ci est envoyée. Sinon, la requête est bloquée.
L’utilisation de access-control-allow-origin: * autorise toutes les origines, ce qui peut être risqué si vous n’en avez pas besoin.
Comment activer CORS ?
Pour activer CORS, il faut configurer le serveur pour qu’il inclue des en-têtes spécifiques autorisant les origines, les méthodes HTTP, les en-têtes personnalisés, les informations d’identification, etc.
- Le navigateur analyse les en-têtes CORS provenant du serveur et n’autorise les requêtes du client qu’après avoir validé leurs paramètres.
Access-Control-Allow-Origin: Permet de définir des domaines spécifiques (ex: https://app.geekflate.com, https://lab.toptips.fr.com) ou d’utiliser un caractère générique.Access-Control-Allow-Methods: Permet de définir les méthodes HTTP (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) autorisées.Access-Control-Allow-Headers: Permet d’autoriser l’envoi de certains en-têtes spécifiques (ex: autorisation, csrf-token).Access-Control-Allow-Credentials: Indique si les informations d’identification (cookies, en-tête d’autorisation) sont autorisées.
Access-Control-Max-Age : Demande au navigateur de mettre en cache la réponse de pré-vérification pendant une période donnée.
Access-Control-Expose-Headers : Détermine quels en-têtes sont accessibles par le script côté client.
Des tutoriels existent pour configurer CORS sur Apache ou Nginx.
Voici un exemple en ExpressJS:
const express = require('express');
const app = express()
app.get('/users', function (req, res, next) {
res.json({msg: 'user get'})
});
app.post('/users', function (req, res, next) {
res.json({msg: 'user create'})
});
app.put('/users', function (req, res, next) {
res.json({msg: 'User update'})
});
app.listen(80, function () {
console.log('Serveur web avec CORS activé, en écoute sur le port 80')
})
Activer CORS dans ExpressJS
Voici un exemple d’application ExpressJS sans CORS :
npm install cors
Dans l’exemple précédent, nous avons activé l’accès à l’API /users pour les méthodes POST, PUT et GET, mais pas DELETE.
Pour simplifier l’activation de CORS dans une application ExpressJS, installez le package ‘cors’ :
app.use(cors({
origin: '*'
}));
Access-Control-Allow-Origin
app.use(cors({
origin: 'https://app.toptips.fr.com'
}));
Activation de CORS pour tous les domaines
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
]
}));
Activation de CORS pour un seul domaine
Pour activer CORS pour https://app.toptips.fr.com et https://lab.toptips.fr.com, utilisez le code suivant :
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST']
}));
Access-Control-Allow-Methods
Pour autoriser toutes les méthodes, vous pouvez omettre l’option ‘methods’. Pour n’autoriser que des méthodes spécifiques (GET, POST, PUT), utilisez la configuration ci-dessus.
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token']
}));
Access-Control-Allow-Headers
Cet en-tête permet d’autoriser l’envoi d’en-têtes spécifiques, en plus des en-têtes par défaut.
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true
}));
Access-Control-Allow-Credentials
L’omission de cet en-tête empêche le navigateur d’autoriser les informations d’identification, même si l’attribut ‘withCredentials’ est défini à ‘true’.
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600
}));
Access-Control-Max-Age
Cette option permet au navigateur de mettre en cache les informations de pré-vérification pendant une durée spécifiée (en secondes). Omettez-la si vous ne souhaitez pas mettre en cache la réponse.
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600,
exposedHeaders: ['Content-Range', 'X-Content-Range']
}));
Dans cet exemple, la réponse de pré-vérification sera mise en cache pendant 10 minutes.
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600,
exposedHeaders: ['*', 'Authorization', ]
}));
Access-Control-Expose-En-têtes
Si le caractère générique est utilisé
dans exposedHeaders, l’en-tête « Authorization » ne sera pas exposé. Il est donc nécessaire de l’exposer explicitement, comme dans l’exemple ci-dessus.
Cette configuration exposera à la fois tous les en-têtes et l’en-tête « Authorization ».
- Qu’est-ce qu’un cookie HTTP ?
- Un cookie est un petit ensemble de données envoyé par le serveur au navigateur client. Lors des requêtes ultérieures, le navigateur transmet tous les cookies liés au domaine concerné.
- Chaque cookie possède des attributs, qui permettent de le configurer en fonction des besoins :
- Nom : Nom du cookie
- Valeur : Données du cookie associées au nom du cookie
- Domaine : Le cookie est transmis uniquement au domaine spécifié
- Chemin : Le cookie est transmis uniquement pour les requêtes dont le chemin d’URL correspond au préfixe défini. Par exemple, si le chemin du cookie est défini comme
'admin/', le cookie ne sera pas transmis pour l’URL https://toptips.fr.com/expire/ mais sera transmis pour l’URL https://toptips.fr.com/admin/ - Max-Age/Expires (en secondes) : Définit la durée de vie du cookie. Une fois ce délai passé, le cookie est considéré comme invalide.
- HTTPOnly (booléen) : Si
true, le cookie n’est accessible que par le serveur backend, et non par le script côté client. - Sécurisé (booléen) : Si
true, le cookie est transmis uniquement sur une connexion SSL/TLS (HTTPS). - MêmeSite (chaîne) : Utilisé pour activer ou restreindre l’envoi de cookies sur des requêtes intersites. Pour en savoir plus sur les cookies du même site, voir MDN. Il accepte trois options : Strict, Lax et None. Pour la configuration
sameSite=None, l’attribut « Sécurisé » doit être défini sur ‘true’.
Pourquoi utiliser des cookies HTTPOnly pour les jetons ?
Le stockage d’un jeton d’accès fourni par le serveur dans le stockage côté client, comme le stockage local, une base de données indexée ou un cookie (sans l’attribut HTTPOnly défini à ‘true’), est plus vulnérable aux attaques XSS (Cross-Site Scripting). Si l’une de vos pages est vulnérable à une attaque XSS, les attaquants peuvent voler les jetons utilisateur stockés dans le navigateur.
Les cookies HTTPOnly ne sont définis/récupérés que par le serveur/backend, et non par le côté client.
- L’accès à un cookie HTTPOnly par un script côté client est interdit, ce qui rend ces cookies plus sécurisés car ils sont moins vulnérables aux attaques XSS. Étant donné qu’ils ne sont accessibles que par le serveur.
- Activation des cookies HTTPOnly avec prise en charge de CORS
- Pour que l’activation des cookies fonctionne avec CORS, il faut configurer l’application/le serveur comme suit :
- Définir l’en-tête
Access-Control-Allow-Credentialsà ‘true’.
Access-Control-Allow-Origin et Access-Control-Allow-Headers ne doivent pas être des caractères génériques.
const express = require('express');
const app = express();
const cors = require('cors');
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600,
exposedHeaders: ['*', 'Authorization' ]
}));
app.post('/login', function (req, res, next) {
res.cookie('access_token', access_token, {
expires: new Date(Date.now() + (3600 * 1000 * 24 * 180 * 1)), //second min hour days year
secure: true, // set to true if your using https or samesite is none
httpOnly: true, // backend only
sameSite: 'none' // set to none for cross-request
});
res.json({ msg: 'Login Successfully', access_token });
});
app.listen(80, function () {
console.log('Serveur web avec CORS activé, en écoute sur le port 80')
});
L’attribut sameSite du cookie doit être défini à ‘None’.
Pour utiliser la valeur ‘none’ pour sameSite, la valeur ‘secure’ doit être définie à ‘true’ : Activez le backend avec un certificat SSL/TLS.
Voici un exemple de code définissant un jeton d’accès dans un cookie HTTPOnly après validation des informations de connexion.
Vous pouvez configurer les cookies CORS et HTTPOnly dans votre langage principal et votre serveur web en suivant les quatre étapes présentées ci-dessus.
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://api.toptips.fr.com/user', true);
xhr.withCredentials = true;
xhr.send(null);
Vous trouverez des tutoriels pour configurer CORS sur Apache et Nginx en suivant les étapes ci-dessus.
fetch('http://api.toptips.fr.com/user', {
credentials: 'include'
});
withCredentials pour les requêtes inter-origines
$.ajax({
url: 'http://api.toptips.fr.com/user',
xhrFields: {
withCredentials: true
}
});
Les identifiants (cookies, autorisation) sont envoyés par défaut avec les requêtes de même origine. Pour les requêtes inter-origines, withCredentials doit être défini à ‘true’.
axios.defaults.withCredentials = true
API XMLHttpRequest
API Fetch
JQuery Ajax
Axios
Conclusion
J’espère que cet article vous a permis de mieux comprendre le fonctionnement de CORS et comment activer CORS pour les requêtes inter-origines. Nous avons vu pourquoi le stockage des jetons dans les cookies HTTPOnly est plus sûr, et comment withCredentials est utilisé par les clients pour les requêtes inter-origines.