[RFC] 8 min read

[RFC] Base64 URL-Safe vs Base64 standard

Le Base64 standard et le Base64 URL-safe se ressemblent presque mais cassent mutuellement leurs décodeurs. Voici exactement ce qui change, pourquoi, et quelle variante il vous faut pour JWT, OAuth et les URL.

April 2026 | standards

// TL;DR

Deux alphabets Base64 existent, tous deux spécifiés dans la RFC 4648.

Base64 standard (§4) — caractères A–Z a–z 0–9 + /, avec remplissage =. Sûr pour MIME, les fichiers PEM, HTTP Basic Auth, les e-mails. Casse lorsqu'il est inséré dans des URL, des cookies, des noms de fichiers ou des étiquettes DNS.

Base64 URL-safe, parfois noté base64url (§5) — caractères A–Z a–z 0–9 - _, remplissage généralement omis. Sûr pour les URL, les parties de JWT, les jetons d'état OAuth, les cookies, le DNS, les noms de fichiers.

Les décodeurs d'un alphabet rejetteront la sortie de l'autre. Choisissez la variante qui correspond à votre transport et tenez-vous-y.

// LES DEUX ALPHABETS, CÔTE À CÔTE

// Position 62 and 63 are the only differences
//
// Standard (RFC 4648 §4)   URL-safe (RFC 4648 §5)
// 0–25   A-Z                A-Z
// 26–51  a-z                a-z
// 52–61  0-9                0-9
// 62     +                  -      (hyphen)
// 63     /                  _      (underscore)
// pad    =                  = or omitted

// Worked example: encoding 0xFB 0xFF 0xBF
// binary: 11111011 11111111 10111111
// 6-bit:  111110 111111 111110 111111
// values: 62     63     62     63
// Standard:  + / + /       → '+/+/'
// URL-safe:  - _ - _       → '-_-_'

// POURQUOI UNE VARIANTE URL-SAFE EST NÉCESSAIRE

L'alphabet standard a été conçu pour les corps MIME, qui sont permissifs. Les URL ne le sont pas. Dans la RFC 3986, + se décode parfois en espace (héritage des formulaires HTML), / est un séparateur de chemin et = est un sous-délimiteur réservé. Si vous placez une chaîne Base64 standard contenant ces trois caractères dans une URL, différents analyseurs ne s'accorderont pas sur sa signification. L'encodage en pourcentage corrige techniquement le problème (+%2B, /%2F, =%3D), mais le résultat est boursouflé et laid.

La RFC 4648 §5 résout le problème en choisissant deux caractères ASCII qui n'ont aucune signification particulière dans les URL : - (tiret) et _ (trait de soulignement). Tous deux sont non réservés selon la RFC 3986 et traversent intacts tout analyseur d'URL. Le remplissage = est également problématique dans certains contextes d'URL (c'est le séparateur clé/valeur dans les chaînes de requête), aussi base64url le supprime-t-il conventionnellement.

// QUAND UTILISER QUOI

  • > Base64 standard — à utiliser pour :
  • > • Pièces jointes d'e-mail MIME (RFC 2045)
  • > • Certificats et clés au format PEM (RFC 7468)
  • > • En-tête HTTP Basic Auth (Authorization: Basic ...)
  • > • data: URIs (data:image/png;base64,...) — les caractères +/= sont tous légaux dans les attributs entre guillemets
  • > • Signatures XML-DSIG et XML-ENC
  • > • S/MIME
  • > Base64 URL-safe — à utiliser pour :
  • > • En-têtes, charges utiles et signatures de JWT (la RFC 7515 impose base64url)
  • > • OAuth 2.0 code_challenge / PKCE (RFC 7636)
  • > • Jetons OpenID Connect state et nonce
  • > • Liens magiques, codes d'invitation et courts jetons d'URL
  • > • Valeurs de cookies qui doivent survivre à une copie dans une URL
  • > • Noms de fichiers générés à partir de hachages (évitez / dans les noms de fichiers !)
  • > • Étiquettes DNS et enregistrements TXT (le tiret est autorisé, la barre oblique ne l'est pas)
  • > • Clés de stockage adressable par contenu (systèmes de type IPFS)

// CONVERSION ENTRE LES DEUX

Puisque les deux alphabets ne diffèrent qu'aux positions 62, 63 et au remplissage, la conversion est un simple rechercher-remplacer à trois caractères. Voici le code dans tous les runtimes que vous êtes susceptible d'utiliser.

// JavaScript (browser + Node)
const toUrlSafe = (b64) =>
  b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

const fromUrlSafe = (b64u) => {
  let s = b64u.replace(/-/g, '+').replace(/_/g, '/');
  while (s.length % 4) s += '=';   // re-add padding
  return s;
};

// Node 16+ native
const url = Buffer.from(data).toString('base64url');
const std = Buffer.from(url, 'base64url').toString('base64');

// Python
import base64
url = base64.urlsafe_b64encode(data).rstrip(b'=')      # no padding
std = base64.b64encode(data)

// Go
import "encoding/base64"
url := base64.RawURLEncoding.EncodeToString(data)  // no padding
std := base64.StdEncoding.EncodeToString(data)

// PHP
$url = rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
$std = base64_encode($data);

// Java
String url = Base64.getUrlEncoder().withoutPadding().encodeToString(data);
String std = Base64.getEncoder().encodeToString(data);

// Shell (GNU coreutils 9+)
echo -n 'hello' | base64 --wrap=0 | tr '+/' '-_' | tr -d '='

// LA QUESTION DU REMPLISSAGE

Le Base64 standard complète toujours la sortie à un multiple de 4 caractères avec =. Le Base64 URL-safe omet généralement le remplissage (la RFC 4648 §5 le précise explicitement comme optionnel). Les JWT (RFC 7515) interdisent totalement le remplissage — eyJhbGciOiJIUzI1NiJ9 n'a pas de = final. Les code challenges OAuth (RFC 7636) interdisent également le remplissage.

Les décodeurs doivent gérer les deux. Un décodeur permissif supprime tout = final et complète en interne si la longueur de la charge utile n'est pas un multiple de 4. Un décodeur strict rejette tout ce qui n'est pas déjà aligné sur 4. Si vous écrivez des encodeurs, alignez-vous sur les attentes du destinataire. Si vous écrivez des décodeurs, soyez généreux.

Combien de remplissage ajouter lors du décodage d'un base64url sans remplissage ? La règle est : si la longueur de l'entrée modulo 4 vaut 2, ajoutez deux = ; si elle vaut 3, ajoutez un = ; si elle vaut 0, n'ajoutez rien ; si elle vaut 1, l'entrée est invalide (aucun base64 valide n'a cette longueur mod 4).

// Restore padding before decoding a JWT segment
function addPadding(b64u) {
  const mod = b64u.length % 4;
  if (mod === 2) return b64u + '==';
  if (mod === 3) return b64u + '=';
  if (mod === 0) return b64u;
  throw new Error('Invalid base64url length');
}

// CE QUI CASSE SI VOUS LES MÉLANGEZ

  • > Corruption silencieuse des données — un décodeur standard alimenté par une charge utile URL-safe décodera la plupart des caractères correctement, mais - et _ ne sont pas dans son alphabet. Selon l'implémentation, il pourra lever une exception, les considérer comme des rebuts (en les sautant), ou produire silencieusement de mauvais octets.
  • > Incohérences de longueur — le décodeur voit une chaîne dont la longueur n'est pas un multiple de 4 (parce que le remplissage a été supprimé) et la rejette purement et simplement. Point de douleur fréquent avec les bibliothèques JWT qui attendent une entrée remplie.
  • > Surprises d'encodage en pourcentage — si vous placez accidentellement du Base64 standard dans une URL et qu'un intergiciel n'encode en pourcentage que + et =, vous obtenez une charge utile avec un encodage mixte : valide pour certains décodeurs, inexploitable pour d'autres.
  • > Rejets de cookies — plusieurs serveurs HTTP rejettent les cookies qui contiennent / ou = dans la valeur, même si la spec l'autorise techniquement. Le Base64 URL-safe contourne cela entièrement.
  • > Échecs de noms de fichiers sous Windows — la sortie Base64 standard contient /, qui est un séparateur de chemin. Nommer un fichier d'après son SHA-256 en Base64 standard échoue à la fois sous Windows et sous POSIX.
  • > Corruption au copier-coller — certains terminaux et applications de chat effectuent un retour à la ligne sur /, cassant une chaîne Base64 standard sur plusieurs lignes de manière invisible. L'URL-safe ne déclenche pas ce phénomène.

// ÉTUDE DE CAS RÉELLE : JWT

Un JSON Web Token ressemble à header.payload.signature, trois chaînes base64url séparées par des points. Toutes les principales bibliothèques JWT (jose, jsonwebtoken, PyJWT, golang-jwt, java-jwt) encodent en base64url sans remplissage, car les jetons sont couramment placés dans des URL et des en-têtes HTTP.

Si vous décodez un JWT à la main avec atob() dans le navigateur, vous devez d'abord convertir : remplacez - par +, _ par /, et ajoutez les = manquants. Des bibliothèques comme jose gèrent cela en interne.

// Decode a JWT payload in the browser
function decodeJwt(token) {
  const [, payloadB64u] = token.split('.');
  let b64 = payloadB64u.replace(/-/g, '+').replace(/_/g, '/');
  while (b64.length % 4) b64 += '=';
  const json = atob(b64);
  return JSON.parse(json);
}

// Example JWT payload
decodeJwt('eyJhbGciOiJIUzI1NiJ9' +
          '.eyJzdWIiOiJhYmMxMjMifQ' +
          '.sig').sub;     // 'abc123'

// DATA URIs : UN CAS PARTICULIER

Les data URI (data:image/png;base64,...) sont techniquement des URL, mais selon la RFC 2397 ils acceptent l'alphabet Base64 standard complet, y compris + et /. C'est pourquoi vous voyez du Base64 standard dans les data URI partout — c'est la spec.

Un Base64 URL-safe à l'intérieur d'un data URI viole techniquement la RFC 2397 et peut être rejeté par des analyseurs stricts. Tenez-vous en au Base64 standard pour les data URI. La seule fois où l'URL-safe importe pour les data URI, c'est si l'URI lui-même est incorporé dans une autre URL en tant que paramètre de requête — auquel cas vous devriez plutôt encoder en pourcentage l'ensemble.

// BASE64URL N'EST PAS L'URL-ENCODING

Il convient de le dire explicitement, car les noms prêtent à confusion. L'URL encoding (également appelé percent-encoding, RFC 3986) transforme les caractères réservés en séquences %XX — c'est ce qui arrive à un espace quand vous le tapez dans la barre d'adresse d'un navigateur (%20).

Le Base64 URL-safe est une variante du Base64 conçue pour ne pas nécessiter d'URL-encoding. Il produit une sortie déjà URL-safe, donc aucun encodage en pourcentage n'est requis. Vous n'appliquez jamais les deux en séquence ; vous appliquez l'un ou l'autre selon vos données.

Règle empirique : si les données sont binaires (une image, un hachage, une signature) et que vous devez les placer dans une URL, utilisez base64url. Si les données sont déjà du texte (une valeur de paramètre de requête, un champ de formulaire), utilisez le percent-encoding.

// ARBRE DE DÉCISION

  • > La chaîne Base64 va-t-elle dans une URL, une valeur de cookie, un nom de fichier, une étiquette DNS, un JWT ou un flux OAuth ? → Base64 URL-safe.
  • > Va-t-elle dans un corps d'e-mail, un fichier PEM, un en-tête HTTP Basic Auth, un data URI ou un corps multipart MIME ? → Base64 standard.
  • > Le consommateur est-il une bibliothèque que vous ne contrôlez pas (analyseur JWT, fournisseur OAuth, client S/MIME) ? → Alignez-vous sur ce qu'ils attendent ; lisez la spec ou la documentation de la bibliothèque.
  • > Pas sûr ? → Utilisez l'URL-safe sans remplissage. C'est la variante la plus portable ; vous pouvez toujours ajouter le remplissage pour les destinataires stricts.

// ESSAYEZ PAR VOUS-MÊME

Notre encodeur Base64 produit du Base64 standard par défaut avec un interrupteur pour la sortie URL-safe. Notre encodeur base64url utilise par défaut l'URL-safe sans remplissage. Utilisez-les côte à côte pour voir exactement quels caractères changent.

Lectures complémentaires :
Qu'est-ce que le Base64 et comment fonctionne-t-il ? — le regroupement sur 6 bits qui sous-tend les deux variantes.
Base64 en JavaScript (navigateur + Node) — utilitaires spécifiques au runtime.
Base64 vs Base64URL vs Base32 — quand aucune des variantes Base64 n'est le bon choix.