Using signed cookies

This page provides an overview of signed cookies and instructions for using them with Cloud CDN.

Signed cookies give time-limited resource access to a set of files, regardless of whether the users have Google accounts.

Overview

Signed cookies are an alternative to signed URLs. Signed cookies protect access when separately signing tens or hundreds of URLs for each user isn't feasible in your application.

Signed cookies allow you to:

  • Authorize a user and provide them with a time-limited token for accessing your protected content (instead of signing each URL).
  • Scope the user's access to a specific URL prefix, such as https://media.example.com/videos/, and grant the authorized user access to protected content within that URL prefix only.
  • Keep your URLs and media manifests unchanged, simplifying your packaging pipeline and improving cacheability.

If you need to scope access to specific URLs instead, consider using signed URLs.

Before you begin

Before you use signed cookies:

  • Ensure that Cloud CDN is enabled. For instructions, see Google Cloud CDN documentation. You can configure signed cookies on a backend before enabling Cloud CDN, but there will be no effect until Cloud CDN is enabled.

  • If necessary, update to the latest version of Cloud SDK:

    gcloud components update
    
  • For an overview, see Signed URLs and signed cookies overview.

Configuring signed request keys

Creating keys for your signed URLs or signed cookies requires several steps, which are described in the following sections.

Security considerations

You must configure your origin web servers to validate the signatures on every signed request they serve and accept or reject unsigned requests.

  • Cloud CDN doesn't block requests without a Signature query parameter. It will reject requests with invalid (or otherwise malformed) request parameters.
  • When your application detects an invalid signature, make sure that your application responds with an HTTP 403 (Unauthorized) response code. HTTP 403 response codes aren't cacheable.
  • If your application sends a cacheable response code to an invalid request, valid future requests might be incorrectly rejected.

For Cloud Storage backends, you should ensure you have removed public access, which will allow Cloud Storage to reject requests missing a valid signature.

Creating signed request keys

You enable support for Cloud CDN signed URLs and signed cookies by creating one or more keys on a Cloud CDN-enabled backend service, backend bucket, or both.

For each backend service or backend bucket, you can create and delete keys as your security needs dictate. Each backend can have up to three keys configured at a time. We suggest periodically rotating your keys by deleting the oldest, adding a new key, and using the new key when signing URLs or cookies.

You can use the same key name in multiple backend services and backend buckets, because each set of keys is independent of the others. Key names can be up to 63 characters. Use the characters A-Z, a-z, 0-9, _ (underscore), and - (hyphen) to name your keys.

When you create keys, be sure to keep them secure because anyone who has one of your keys can create signed URLs or signed cookies that Cloud CDN accepts until the key is deleted from Cloud CDN. The keys are stored on the computer where you generate the signed URLs or signed cookies. Cloud CDN also stores the keys to verify request signatures.

To keep the keys secret, the key values aren't included in responses to any API requests. If you lose a key, you must create a new one.

Console

To create keys using the Cloud Console:

  1. Go to the Cloud CDN page in the Google Cloud Console.
    Go to the Cloud CDN page
  2. Click Add Origin.
  3. Select an HTTP(S) load balancer as the origin.
  4. Select backend services or backend buckets. For each one:
    1. Click Configure and then click Add signing key.
    2. Under Name, give the new signing key a name.
    3. Under Key creation method, elect Automatically generate or Let me enter.
    4. If you're entering your own key, type the key into the text field.
    5. Click Done.
    6. Under Cache entry maximum age, provide a value, and select a Unit of time from the drop-down list. You can choose among second, minute, hour, and day. The maximum amount of time is three (3) days.
  5. Click Save.
  6. Click Add.

gcloud

The gcloud command line tool reads keys from a local file that you specify. The key file must be created by generating strongly random 128 bits, encoding them with base64, then replacing the character + with - and replacing the character / with _. For more information, see RFC 4648. It is vital that the key is strongly random. On a UNIX-like system, you can generate a strongly random key and store it in the key file with the following command:

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

To add the key to a backend service:

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

To add the key to a backend bucket:

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

Configuring Cloud Storage permissions

If you use Cloud Storage and you have restricted who can read the objects, you must give Cloud CDN permission to read the objects by adding the Cloud CDN service account to Cloud Storage's ACLs.

You don't need to create the service account. The service account is created automatically the first time you add a key to a backend bucket in a project.

Use the following command, where [PROJECT_NUM] is your project number and [BUCKET] is your storage bucket.

Run the command after you add at least one key to a backend bucket in your project. Otherwise, the command fails with an error because the Cloud CDN cache fill service account is not created until you add one or more keys for the project.

gsutil iam ch \
  serviceAccount:service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com:objectViewer \
  gs://[BUCKET]

The Cloud CDN service account, service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com, doesn't appear in the list of service accounts in your project. This is because the Cloud CDN service account is owned by Cloud CDN, not your project.

For more information on project numbers, see Locate the project ID and project number in the Cloud Platform Console Help documentation.

Optionally customizing the maximum cache time

Cloud CDN caches responses for signed requests regardless of the backend's Cache-Control header. The maximum time they can be cached without revalidation is set by the signed-url-cache-max-age flag, which defaults to 1 hour and can be modified as shown here.

To set the maximum cache time for a backend service or backend bucket:

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]

Listing signed request key names

To list the keys on a backend service or backend bucket, use these commands.

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

Deleting signed request keys

When URLs signed by a particular key should no longer be honored, delete that key from the backend service or backend bucket.

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]

Creating a policy

Signed cookie policies are a series of key=value pairs (delimited by the : character), similar to the query parameters used in a signed URL. For examples, see Issuing cookies to users.

Policies represent the parameters for which a request is valid. Policies are signed using a hash-based message authentication code (HMAC) , which Cloud CDN validates on each request.

Policy format and fields

There are four fields, they are all required, and you must define them in the following order:

  • URLPrefix
  • Expires
  • KeyName
  • Signature

The key=value pairs in a signed cookie policy are case-sensitive.

URLPrefix

URLPrefix denotes a URL-safe base64 encoded URL prefix encompassing all paths that the cookie should be valid for.

A URLPrefix encodes a scheme (either http:// or https://), FQDN, and an optional path. Ending the path with a / is optional but recommended. The prefix shouldn't include query parameters or fragments such as ? or #.

For example, https://media.example.com/videos matches requests to both:

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

The prefix's path is used as a text substring, not strictly a directory path. For example, the prefix https://example.com/data grants access to both:

  • /data/file1
  • /database

We recommend ending all prefixes with / to avoid this mistake, unless you intentionally choose to end the prefix with a partial filename, such as https://media.example.com/videos/123 to grant access to:

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

If the requested URL doesn't match the URLPrefix, Cloud CDN rejects the request and returns an HTTP 403 error to the client.

Expires

Expires must be a Unix timestamp (the number of seconds since Jan 1, 1970).

KeyName

KeyName is the key name for a key created against the backend bucket or service. Key names are case-sensitive.

Signature

Signature is the URL-safe base64 encoded HMAC-SHA-1 signature of the fields that make up the cookie policy. This is validated on each request; requests with an invalid signature are rejected with an HTTP 403 error.

Programmatically creating signed cookies

The following code samples demonstrate how to programmatically create signed cookies.

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
}

// readKeyFile reads the base64url-encoded key file and decodes it.
func readKeyFile(path string) ([]byte, error) {
	b, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read key file: %+v", err)
	}
	d := make([]byte, base64.URLEncoding.DecodedLen(len(b)))
	n, err := base64.URLEncoding.Decode(d, b)
	if err != nil {
		return nil, fmt.Errorf("failed to base64url decode: %+v", err)
	}
	return d[:n], nil
}

func generateSignedCookie(w io.Writer) error {
	// The path to a file containing the base64-encoded signing key
	keyPath := os.Getenv("KEY_PATH")

	// Note: consider using the GCP Secret Manager for managing access to your
	// signing key(s).
	key, err := readKeyFile(keyPath)
	if err != nil {
		return err
	}

	var (
		// domain and path should match the user-facing URL for accessing
		// content.
		domain     = "media.example.com"
		path       = "/segments/"
		keyName    = "my-key"
		expiration = time.Hour * 2
	)

	signedValue, err := signCookie(fmt.Sprintf("https://%s%s", domain,
		path), keyName, key, time.Now().Add(expiration))
	if err != nil {
		return err
	}

	// Use Go's http.Cookie type to construct a cookie.
	cookie := &http.Cookie{
		Name:   "Cloud-CDN-Cookie",
		Value:  signedValue,
		Path:   path, // Best practice: only send the cookie for paths it is valid for
		Domain: domain,
		MaxAge: int(expiration.Seconds()),
	}

	// We print this to stdout in this example. In a real application, use the
	// SetCookie method on a http.ResponseWriter to write the cookie to the
	// user.
	fmt.Fprintln(w, cookie)

	return nil
}

Python

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

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

    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.datetime.utcfromtimestamp(0)
    expiration_timestamp = int((expiration_time - epoch).total_seconds())
    decoded_key = base64.urlsafe_b64decode(base64_key)

    policy_pattern = u'URLPrefix={encoded_url_prefix}:Expires={expires}:KeyName={key_name}'
    policy = policy_pattern.format(
            encoded_url_prefix=encoded_url_prefix,
            expires=expiration_timestamp,
            key_name=key_name)

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

    signed_policy = u'Cloud-CDN-Cookie={policy}:Signature={signature}'.format(
            policy=policy, signature=signature)
    print(signed_policy)

Issuing cookies to users

Your application must generate and issue each user (client) a single HTTP cookie containing a correctly signed policy.

  1. Create a HMAC-SHA-1 signer in your application code.

  2. Sign the policy using the chosen key, taking note of the key name that you added to the backend, such as mySigningKey.

  3. Create a cookie policy with the following format, noting that both the name and value are case-sensitive.

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

    Example Set-Cookie header:

    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
    

    The Domain and Path attributes in the cookie determine whether the client sends the cookie to Cloud CDN.

Recommendations and requirements

  • Explicitly set the Domain and Path attributes to match the domain and path prefix from which you intend to serve your protected content, which might differ from the domain and path where the cookie is issued (example.com versus media.example.com or /browse versus /videos).

  • Make sure that you have only one cookie with a given name for the same Domain and Path.

  • Ensure that you aren't issuing conflicting cookies, because this might prevent access to content in other browser sessions (windows or tabs).

  • Set the Secure and HttpOnly flags where applicable. Secure ensures that the cookie is sent over HTTPS connections only. HttpOnly prevents making the cookie available to JavaScript.

  • The cookie Expires and Max-Age attributes are optional. If you omit them, the cookie exists while the browser session (tab, window) exists.

  • On a cache fill or cache miss, the signed cookie is passed through to the origin that is defined in the backend service. Ensure that you are validating the signed cookie value on each request before serving content.