Utiliser des cookies signés

Cette page présente les cookies signés et décrit comment les utiliser avec Cloud CDN. Les cookies signés fournissent un accès limité dans le temps aux ressources d'un ensemble de fichiers, que les utilisateurs possèdent ou non un compte Google.

Les cookies signés constituent une alternative aux URL signées. Ils protègent l'accès lorsqu'il n'est pas envisageable de signer séparément des dizaines ou des centaines d'URL pour chaque utilisateur dans votre application.

Les cookies signés vous permettent :

  • d'autoriser un utilisateur et de lui fournir un jeton limité dans le temps pour qu'il puisse accéder à votre contenu protégé (au lieu de signer chaque URL) ;
  • de limiter l'accès de l'utilisateur à un préfixe d'URL spécifique, tel que https://media.example.com/videos/, et d'accorder à l'utilisateur autorisé un accès au contenu protégé au sein de ce préfixe d'URL uniquement ;
  • de conserver vos URL et fichiers manifestes multimédias tels quels, ce qui simplifie le pipeline d'empaquetage et améliore la mise en cache.

Si vous préférez limiter l'accès à des URL spécifiques, envisagez plutôt d'utiliser des URL signées.

Avant de commencer

Avant d'utiliser des cookies signés, procédez comme suit :

  • Assurez-vous que Cloud CDN est activé. Pour obtenir des instructions, consultez la page Utiliser Cloud CDN. Vous pouvez configurer des cookies signés sur un backend avant d'activer Cloud CDN, mais ils sont sans effet jusqu'à l'activation de Cloud CDN.

  • Si nécessaire, installez la dernière version de Google Cloud CLI :

    gcloud components update
    

Pour en savoir plus, consultez la page Cookies et URL signés.

Configurer des clés de requête signées

La création de clés pour vos URL ou cookies signés nécessite plusieurs étapes, décrites dans les sections suivantes.

Points à noter concernant la sécurité

Cloud CDN ne valide pas les requêtes dans les cas suivants :

  • La requête n'est pas signée.
  • Cloud CDN n'est pas activé sur le service de backend ou le bucket backend de la requête.

Les requêtes signées doivent toujours être validées à l'origine avant de diffuser la réponse. Ceci est dû au fait que les origines peuvent servir à diffuser un mélange de contenu signé et non signé, et qu'un client peut accéder directement à l'origine.

  • Cloud CDN ne bloque pas les requêtes n'utilisant ni le paramètre de requête Signature, ni le cookie HTTP Cloud-CDN-Cookie. Cloud CDN rejette les requêtes dont les paramètres sont incorrects (ou ne sont pas rédigés correctement).
  • Lorsque votre application détecte une signature non valide, assurez-vous qu'elle répond avec un code de réponse HTTP 403 (Unauthorized). Les codes de réponse HTTP 403 ne peuvent pas être mis en cache.
  • Les réponses aux requêtes signées et non signées sont mises en cache séparément. Par conséquent, une réponse réussie à une requête signée valide n'est jamais utilisée pour diffuser une requête non signée.
  • Si votre application envoie un code de réponse pouvant être mis en cache à une requête non valide, les futures requêtes valides risquent d'être rejetées par erreur.

Pour les backends Cloud Storage, assurez-vous de supprimer l'accès public afin que Cloud Storage puisse rejeter les requêtes ne contenant pas de signature valide.

Le tableau suivant récapitule le comportement.

La requête comporte une signature Succès de cache (hit) Comportement
Non Non La requête est transférée vers l'origine du backend.
Non Oui La requête est diffusée à partir du cache.
Oui Non La signature est validée. Si la requête est valide, elle est transférée vers l'origine du backend.
Oui Oui La signature est validée. Si la requête est valide, elle est diffusée à partir du cache.

Créer des clés de requête signées

Pour pouvoir exploiter les URL et cookies signés Cloud CDN, vous devez créer une ou plusieurs clés sur un service de backend et/ou un bucket backend compatibles avec Cloud CDN.

Pour chaque service de backend ou bucket backend, vous pouvez créer et supprimer des clés selon vos besoins en termes de sécurité. Chaque backend peut comporter jusqu'à trois clés configurées à la fois. Nous vous suggérons d'effectuer régulièrement une rotation des clés : supprimez la clé la plus ancienne, ajoutez-en une nouvelle et utilisez-la pour la signature des URL ou cookies.

Vous pouvez utiliser le même nom de clé dans plusieurs services de backend et buckets backend, car chaque ensemble de clés est indépendant des autres. Les noms de clé peuvent comporter jusqu'à 63 caractères. Pour nommer vos clés, utilisez les caractères A-Z, a-z, 0-9, _ (trait de soulignement) et - (trait d'union).

Lorsque vous créez des clés, veillez à les sécuriser, car toute personne qui en possède une peut créer des URL ou cookies signés acceptés par Cloud CDN jusqu'à ce que la clé soit supprimée du réseau de diffusion de contenu. Les clés sont stockées sur l'ordinateur sur lequel vous générez les URL ou cookies signés. Cloud CDN stocke également les clés pour valider les signatures de requête.

Pour garder les clés secrètes, les valeurs de clé ne sont pas incluses dans les réponses aux requêtes API. Si vous perdez une clé, vous devez en créer une nouvelle.

Pour créer une clé de requête signée, procédez comme suit :

Console

  1. Dans Google Cloud Console, accédez à la page Cloud CDN.

    Accéder à Cloud CDN

  2. Cliquez sur le nom de l'origine à laquelle vous souhaitez ajouter la clé.
  3. Sur la page Détails de l'origine, cliquez sur le bouton Modifier.
  4. Dans la section Paramètres de base de l'origine, cliquez sur Suivant pour ouvrir la section Règles d'hôte et de chemin d'accès.
  5. Dans la section Règles d'hôte et de chemin d'accès, cliquez sur Suivant pour ouvrir la section Performances du cache.
  6. Dans la section Contenu limité, sélectionnez Restreindre l'accès à l'aide des URL et des cookies signés.
  7. Cliquez sur Ajouter une clé de signature.

    1. Spécifiez un nom unique pour la nouvelle clé de signature.
    2. Dans la section Méthode de création de clé, sélectionnez Générer automatiquement. Vous pouvez également cliquer sur Me laisser saisir, puis spécifier une valeur de clé de signature.

      Pour la première option, copiez la valeur de la clé de signature générée automatiquement dans un fichier privé, que vous pouvez utiliser pour créer des URL signées.

    3. Cliquez sur OK.

    4. Dans la section Âge maximal de l'entrée de cache, saisissez une valeur, puis sélectionnez une unité de temps.

  8. Cliquez sur OK.

gcloud

L'outil de ligne de commande gcloud lit les clés à partir d'un fichier local que vous spécifiez. Le fichier de clé doit être créé en générant une valeur de 128 bits fortement aléatoire, en l'encodant en base64, puis en remplaçant le caractère + par - et le caractère / par _. Pour plus d'informations, consultez la norme RFC 4648. Il est essentiel que la clé soit fortement aléatoire. Sur un système de type UNIX, vous pouvez générer une clé fortement aléatoire et la stocker dans le fichier de clés à l'aide de la commande suivante :

head -c 16 /dev/urandom | base64 | tr +/ -_ > KEY_FILE_NAME

Pour ajouter la clé à un service de backend, utilisez la commande suivante :

gcloud compute backend-services \
   add-signed-url-key BACKEND_NAME \
   --key-name KEY_NAME \
   --key-file KEY_FILE_NAME

Pour ajouter la clé à un bucket backend, utilisez la commande ci-dessous :

gcloud compute backend-buckets \
   add-signed-url-key BACKEND_NAME \
   --key-name KEY_NAME \
   --key-file KEY_FILE_NAME

Configurer les autorisations Cloud Storage

Si vous utilisez Cloud Storage et que vous avez restreint l'accès en lecture aux objets, vous devez autoriser Cloud CDN à lire les objets en ajoutant le compte de service Cloud CDN aux listes de contrôle d'accès Cloud Storage.

Vous n'avez pas besoin de créer le compte de service. Il est créé automatiquement la première fois que vous ajoutez une clé à un bucket backend d'un projet.

Avant d'exécuter la commande suivante, ajoutez au moins une clé à un bucket backend de votre projet. Si vous ne respectez pas cette condition, la commande échoue et renvoie une erreur, car le compte de service du remplissage du cache Cloud CDN n'est pas créé tant que vous n'avez pas ajouté au moins une clé pour le projet.

gcloud storage buckets add-iam-policy-binding gs://BUCKET \
  --member=serviceAccount:service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com \
  --role=roles/storage.objectViewer

Remplacez PROJECT_NUM par le numéro de votre projet et BUCKET par votre bucket de stockage.

Le compte de service Cloud CDN, service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com, ne figure pas dans la liste des comptes de service de votre projet. Ceci est dû au fait qu'il appartient à Cloud CDN, et non à votre projet.

Pour plus d'informations sur les numéros de projet, consultez la section Localiser l'ID et le numéro de projet dans la documentation d'aide de Google Cloud Console.

Personnaliser le délai de cache maximal

Cloud CDN met en cache les réponses aux requêtes signées, quel que soit l'en-tête Cache-Control du backend. Le délai maximal de mise en cache des réponses sans revalidation est défini par l'option signed-url-cache-max-age, qui prend la valeur par défaut d'une heure, et peut être modifié comme indiqué ici.

Pour définir la durée maximale de mise en cache d'un service de backend ou d'un bucket backend, exécutez l'une des commandes suivantes :

gcloud compute backend-services update BACKEND_NAME
  --signed-url-cache-max-age MAX_AGE
gcloud compute backend-buckets update BACKEND_NAME
  --signed-url-cache-max-age MAX_AGE

Répertorier les noms des clés de requête signées

Pour répertorier les clés d'un service de backend ou d'un bucket backend, exécutez l'une des commandes suivantes :

gcloud compute backend-services describe BACKEND_NAME
gcloud compute backend-buckets describe BACKEND_NAME

Supprimer des clés de requête signées

Lorsque les URL signées par une clé spécifique ne doivent plus être honorées, exécutez l'une des commandes suivantes pour supprimer cette clé du service de backend ou du bucket backend :

gcloud compute backend-services \
   delete-signed-url-key BACKEND_NAME --key-name KEY_NAME
gcloud compute backend-buckets \
   delete-signed-url-key BACKEND_NAME --key-name KEY_NAME

Créer une règle

Les règles de cookie signé correspondent à une série de paires key-value (délimitées par le caractère :), semblables aux paramètres de requête utilisés dans une URL signée. Pour obtenir des exemples, consultez la section Émettre des cookies pour les utilisateurs.

Les règles représentent les paramètres pour lesquels une requête est valide. Les règles sont signées à l'aide d'un code d'authentification de message basé sur le hachage (HMAC) que Cloud CDN valide pour chaque requête.

Définir le format et les champs des règles

Vous devez définir quatre champs dans l'ordre suivant :

  • URLPrefix
  • Expires
  • KeyName
  • Signature

Les paires key-value d'une règle de cookie signé sont sensibles à la casse.

URLPrefix

URLPrefix désigne un préfixe d'URL encodé en base64 et sécurisé pour les URL qui englobe tous les chemins pour lesquels la signature doit être valide.

Un préfixe URLPrefix encode un schéma (http:// ou https://), un nom de domaine complet et un chemin d'accès facultatif. Vous n'avez pas besoin de terminer l'URL par le signe /, mais cette pratique est recommandée. Le préfixe ne doit pas inclure de paramètres de requête ni de fragments tels que ? ou #.

Par exemple, https://media.example.com/videos correspond aux requêtes vers les deux éléments suivants :

  • https://media.example.com/videos?video_id=138183&user_id=138138
  • https://media.example.com/videos/137138595?quality=low

Le chemin du préfixe est utilisé en tant que sous-chaîne de texte, et non en tant que chemin de répertoire strict. Par exemple, le préfixe https://example.com/data accorde l'accès à ces deux éléments :

  • /data/file1
  • /database

Pour éviter cette erreur, nous vous recommandons de terminer tous les préfixes par /, sauf si vous choisissez de terminer intentionnellement un préfixe par un nom de fichier partiel tel que https://media.example.com/videos/123 pour accorder l'accès aux éléments suivants :

  • /videos/123_chunk1
  • /videos/123_chunk2
  • /videos/123_chunkN

Si l'URL demandée ne correspond pas à URLPrefix, Cloud CDN rejette la requête et renvoie une erreur HTTP 403 au client.

Expiration

Expires doit être un horodatage Unix (nombre de secondes depuis le 1er janvier 1970).

KeyName

KeyName est le nom d'une clé créée sur le bucket backend ou service de backend. Les noms de clé sont sensibles à la casse.

Signature

Signature est la signature HMAC-SHA-1 sécurisée pour les URL et encodée en base64 des champs qui forment la règle de cookie. Elle est validée à chaque requête. Les requêtes comportant une signature non valide sont rejetées avec une erreur HTTP 403.

Créer des cookies signés par programmation

Les exemples de code suivants montrent comment créer des cookies signés par programmation.

Go

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

// signCookie creates a signed cookie for an endpoint served by Cloud CDN.
//
// - urlPrefix must start with "https://" and should include the path prefix
// for which the cookie will authorize access to.
// - key should be in raw form (not base64url-encoded) which is
// 16-bytes long.
// - keyName must match a key added to the backend service or bucket.
func signCookie(urlPrefix, keyName string, key []byte, expiration time.Time) (string, error) {
	encodedURLPrefix := base64.URLEncoding.EncodeToString([]byte(urlPrefix))
	input := fmt.Sprintf("URLPrefix=%s:Expires=%d:KeyName=%s",
		encodedURLPrefix, expiration.Unix(), keyName)

	mac := hmac.New(sha1.New, key)
	mac.Write([]byte(input))
	sig := base64.URLEncoding.EncodeToString(mac.Sum(nil))

	signedValue := fmt.Sprintf("%s:Signature=%s",
		input,
		sig,
	)

	return signedValue, nil
}

Java

import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.time.ZonedDateTime;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class SignedCookies {

  public static void main(String[] args) throws Exception {
    // TODO(developer): Replace these variables before running the sample.

    // The name of the signing key must match a key added to the back end bucket or service.
    String keyName = "YOUR-KEY-NAME";
    // Path to the URL signing key uploaded to the backend service/bucket.
    String keyPath = "/path/to/key";
    // The Unix timestamp that the signed URL expires.
    long expirationTime = ZonedDateTime.now().plusDays(1).toEpochSecond();
    // URL prefix to sign as a string. URL prefix must start with either "http://" or "https://"
    // and must not include query parameters.
    String urlPrefix = "https://media.example.com/videos/";

    // Read the key as a base64 url-safe encoded string, then convert to byte array.
    // Key used in signing must be in raw form (not base64url-encoded).
    String base64String = new String(Files.readAllBytes(Paths.get(keyPath)),
        StandardCharsets.UTF_8);
    byte[] keyBytes = Base64.getUrlDecoder().decode(base64String);

    // Create signed cookie from policy.
    String signedCookie = signCookie(urlPrefix, keyBytes, keyName, expirationTime);
    System.out.println(signedCookie);
  }

  // Creates a signed cookie for the specified policy.
  public static String signCookie(String urlPrefix, byte[] key, String keyName,
      long expirationTime)
      throws InvalidKeyException, NoSuchAlgorithmException {

    // Validate input URL prefix.
    try {
      URL validatedUrlPrefix = new URL(urlPrefix);
      if (!validatedUrlPrefix.getProtocol().startsWith("http")) {
        throw new IllegalArgumentException(
            "urlPrefix must start with either http:// or https://: " + urlPrefix);
      }
      if (validatedUrlPrefix.getQuery() != null) {
        throw new IllegalArgumentException("urlPrefix must not include query params: " + urlPrefix);
      }
    } catch (MalformedURLException e) {
      throw new IllegalArgumentException(
          "urlPrefix malformed: " + urlPrefix);
    }

    String encodedUrlPrefix = Base64.getUrlEncoder().encodeToString(urlPrefix.getBytes(
        StandardCharsets.UTF_8));
    String policyToSign = String.format("URLPrefix=%s:Expires=%d:KeyName=%s", encodedUrlPrefix,
        expirationTime, keyName);

    String signature = getSignatureForUrl(key, policyToSign);
    return String.format("Cloud-CDN-Cookie=%s:Signature=%s", policyToSign, signature);
  }

  // Creates signature for input string with private key.
  private static String getSignatureForUrl(byte[] privateKey, String input)
      throws InvalidKeyException, NoSuchAlgorithmException {

    final String algorithm = "HmacSHA1";
    final int offset = 0;
    Key key = new SecretKeySpec(privateKey, offset, privateKey.length, algorithm);
    Mac mac = Mac.getInstance(algorithm);
    mac.init(key);
    return Base64.getUrlEncoder()
        .encodeToString(mac.doFinal(input.getBytes(StandardCharsets.UTF_8)));
  }
}

Python

import argparse
import base64
from datetime import datetime, timezone
import hashlib
import hmac
from urllib.parse import parse_qs, urlsplit


def sign_cookie(
    url_prefix: str,
    key_name: str,
    base64_key: str,
    expiration_time: datetime,
) -> str:
    """Gets the Signed cookie value for the specified URL prefix and configuration.

    Args:
        url_prefix: URL prefix to sign.
        key_name: name of the signing key.
        base64_key: signing key as a base64 encoded string.
        expiration_time: expiration time as time-zone aware datetime.

    Returns:
        Returns the Cloud-CDN-Cookie value based on the specified configuration.
    """
    encoded_url_prefix = base64.urlsafe_b64encode(
        url_prefix.strip().encode("utf-8")
    ).decode("utf-8")
    epoch = datetime.fromtimestamp(0, timezone.utc)
    expiration_timestamp = int((expiration_time - epoch).total_seconds())
    decoded_key = base64.urlsafe_b64decode(base64_key)

    policy = f"URLPrefix={encoded_url_prefix}:Expires={expiration_timestamp}:KeyName={key_name}"

    digest = hmac.new(decoded_key, policy.encode("utf-8"), hashlib.sha1).digest()
    signature = base64.urlsafe_b64encode(digest).decode("utf-8")

    signed_policy = f"Cloud-CDN-Cookie={policy}:Signature={signature}"

    return signed_policy

Valider des cookies signés

Le processus de validation d'un cookie signé est fondamentalement le même que celui qui permet de générer un cookie signé. Par exemple, supposons que vous souhaitiez valider l'en-tête de cookie signé suivant :

Cookie: Cloud-CDN-Cookie=URLPrefix=URL_PREFIX:Expires=EXPIRATION:KeyName=KEY_NAME:Signature=SIGNATURE; Domain=media.example.com; Path=/; Expires=Tue, 20 Aug 2019 02:26:49 GMT; HttpOnly

Vous pouvez utiliser la clé secrète nommée KEY_NAME pour générer indépendamment la signature, puis confirmer qu'elle correspond à SIGNATURE.

Émettre des cookies pour les utilisateurs

Votre application doit générer et émettre pour chaque utilisateur (client) un cookie HTTP unique contenant une règle correctement signée.

  1. Créez un signataire HMAC-SHA-1 dans votre code d'application.

  2. Signez la règle à l'aide de la clé choisie, en notant le nom de la clé que vous avez ajoutée au backend (par exemple, mySigningKey).

  3. Créez une règle de cookie au format suivant, en tenant compte du fait que le nom et la valeur sont sensibles à la casse :

    Name: Cloud-CDN-Cookie
    Value: URLPrefix=$BASE64URLECNODEDURLORPREFIX:Expires=$TIMESTAMP:KeyName=$KEYNAME:Signature=$BASE64URLENCODEDHMAC
    

    Exemple d'en-tête Set-Cookie :

    Set-Cookie: Cloud-CDN-Cookie=URLPrefix=aHR0cHM6Ly9tZWRpYS5leGFtcGxlLmNvbS92aWRlb3Mv:Expires=1566268009:KeyName=mySigningKey:Signature=0W2xlMlQykL2TG59UZnnHzkxoaw=; Domain=media.example.com; Path=/; Expires=Tue, 20 Aug 2019 02:26:49 GMT; HttpOnly
    

    Les attributs Domain et Path du cookie déterminent si le client l'enverra à Cloud CDN ou non.

Recommandations et exigences

  • Définissez explicitement les attributs Domain et Path pour qu'ils correspondent au préfixe du domaine et du chemin depuis lesquels vous souhaitez diffuser votre contenu protégé. Ceux-ci peuvent différer du domaine et du chemin où le cookie est émis (example.com par rapport à media.example.com ou /browse par rapport à /videos).

  • Assurez-vous que vous n'avez qu'un cookie avec un nom donné pour les mêmes attributs Domain et Path.

  • Assurez-vous que vous n'émettez pas de cookies en conflit, car cela pourrait empêcher l'accès au contenu dans d'autres sessions de navigateur (fenêtres ou onglets).

  • Définissez les options Secure et HttpOnly, le cas échéant. L'option Secure garantit que le cookie n'est envoyé que via des connexions HTTPS. L'option HttpOnly empêche que le cookie soit mis à disposition dans JavaScript.

  • Les attributs de cookie Expires et Max-Age sont facultatifs. Si vous les omettez, le cookie existera tant que la session de navigateur (onglet ou fenêtre) sera active.

  • Lors d'un remplissage ou d'un défaut de cache (miss), le cookie signé est transmis à l'origine définie dans le service de backend. Assurez-vous de valider la valeur du cookie signé sur chaque requête avant de diffuser du contenu.