Generate tokens

This guide explains how to generate a token, and the required and optional fields for tokens.

To create a token, you compose a string to sign, which we refer to as a signed value in this guide. The signed value includes parameters that describe the content you are protecting, the expiration time of the signed value, and so forth.

You use the signed value while creating a token string. You create a token string by composing the parameters for the token, such as a symmetric-key hash-based message authentication code (HMAC) of the signed value.

Media CDN uses the final composed token to help protect your content.

Create a token

  1. Create a signed value by concatenating a string that contains the required token fields and desired optional token fields. Separate each field and any parameters with a tilde ~ character.

  2. Sign the signed value with either an Ed25519 signature or a symmetric-key HMAC.

  3. Compose the token by concatenating a string that contains the required token fields and optional token fields. Separate each field and any parameters with a tilde ~ character.

    When composing the token, the values for each of the parameters are the same between the signed value and the token string, with the following exceptions:

    • FullPath
    • Headers

The following code sample shows how to programmatically create a token:

Python

To authenticate to Media CDN, set up Application Default Credentials. For more information, see Set up authentication for a local development environment.

import base64
import datetime
import hashlib
import hmac

import cryptography.hazmat.primitives.asymmetric.ed25519 as ed25519


def base64_encoder(value: bytes) -> str:
    """
    Returns a base64-encoded string compatible with Media CDN.

    Media CDN uses URL-safe base64 encoding and strips off the padding at the
    end.
    """
    encoded_bytes = base64.urlsafe_b64encode(value)
    encoded_str = encoded_bytes.decode("utf-8")
    return encoded_str.rstrip("=")


def sign_token(
    base64_key: bytes,
    signature_algorithm: str,
    start_time: datetime.datetime = None,
    expiration_time: datetime.datetime = None,
    url_prefix: str = None,
    full_path: str = None,
    path_globs: str = None,
    session_id: str = None,
    data: str = None,
    headers: str = None,
    ip_ranges: str = None,
) -> str:
    """Gets the Signed URL Suffix string for the Media CDN' Short token URL requests.
    One of (`url_prefix`, `full_path`, `path_globs`) must be included in each input.
    Args:
        base64_key: Secret key as a base64 encoded string.
        signature_algorithm: Algorithm can be either `SHA1` or `SHA256` or `Ed25519`.
        start_time: Start time as a UTC datetime object.
        expiration_time: Expiration time as a UTC datetime object. If None, an expiration time 1 hour from now will be used.
        url_prefix: the URL prefix to sign, including protocol.
                    For example: http://example.com/path/ for URLs under /path or http://example.com/path?param=1
        full_path:  A full path to sign, starting with the first '/'.
                    For example: /path/to/content.mp4
        path_globs: a set of ','- or '!'-delimited path glob strings.
                    For example: /tv/*!/film/* to sign paths starting with /tv/ or /film/ in any URL.
        session_id: a unique identifier for the session
        data: data payload to include in the token
        headers: header name and value to include in the signed token in name=value format.  May be specified more than once.
                    For example: [{'name': 'foo', 'value': 'bar'}, {'name': 'baz', 'value': 'qux'}]
        ip_ranges: A list of comma separated ip ranges. Both IPv4 and IPv6 ranges are acceptable.
                    For example: "203.0.113.0/24,2001:db8:4a7f:a732/64"

    Returns:
        The Signed URL appended with the query parameters based on the
        specified URL prefix and configuration.
    """

    decoded_key = base64.urlsafe_b64decode(base64_key)
    algo = signature_algorithm.lower()

    # For most fields, the value we put in the token and the value we must sign
    # are the same.  The FullPath and Headers use a different string for the
    # value to be signed compared to the token.  To illustrate this difference,
    # we'll keep the token and the value to be signed separate.
    tokens = []
    to_sign = []

    # check for `full_path` or `path_globs` or `url_prefix`
    if full_path:
        tokens.append("FullPath")
        to_sign.append(f"FullPath={full_path}")
    elif path_globs:
        path_globs = path_globs.strip()
        field = f"PathGlobs={path_globs}"
        tokens.append(field)
        to_sign.append(field)
    elif url_prefix:
        field = "URLPrefix=" + base64_encoder(url_prefix.encode("utf-8"))
        tokens.append(field)
        to_sign.append(field)
    else:
        raise ValueError(
            "User Input Missing: One of `url_prefix`, `full_path` or `path_globs` must be specified"
        )

    # check & parse optional params
    if start_time:
        epoch_duration = start_time.astimezone(
            tz=datetime.timezone.utc
        ) - datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)
        field = f"Starts={int(epoch_duration.total_seconds())}"
        tokens.append(field)
        to_sign.append(field)

    if not expiration_time:
        expiration_time = datetime.datetime.now() + datetime.timedelta(hours=1)
        epoch_duration = expiration_time.astimezone(
            tz=datetime.timezone.utc
        ) - datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)
    else:
        epoch_duration = expiration_time.astimezone(
            tz=datetime.timezone.utc
        ) - datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)
    field = f"Expires={int(epoch_duration.total_seconds())}"
    tokens.append(field)
    to_sign.append(field)

    if session_id:
        field = f"SessionID={session_id}"
        tokens.append(field)
        to_sign.append(field)

    if data:
        field = f"Data={data}"
        tokens.append(field)
        to_sign.append(field)

    if headers:
        header_names = []
        header_pairs = []
        for each in headers:
            header_names.append(each["name"])
            header_pairs.append("%s=%s" % (each["name"], each["value"]))
        tokens.append(f"Headers={','.join(header_names)}")
        to_sign.append(f"Headers={','.join(header_pairs)}")

    if ip_ranges:
        field = f"IPRanges={base64_encoder(ip_ranges.encode('ascii'))}"
        tokens.append(field)
        to_sign.append(field)

    # generating token
    to_sign = "~".join(to_sign)
    to_sign_bytes = to_sign.encode("utf-8")
    if algo == "ed25519":
        digest = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key).sign(
            to_sign_bytes
        )
        tokens.append("Signature=" + base64_encoder(digest))
    elif algo == "sha256":
        signature = hmac.new(
            decoded_key, to_sign_bytes, digestmod=hashlib.sha256
        ).hexdigest()
        tokens.append("hmac=" + signature)
    elif algo == "sha1":
        signature = hmac.new(
            decoded_key, to_sign_bytes, digestmod=hashlib.sha1
        ).hexdigest()
        tokens.append("hmac=" + signature)
    else:
        raise ValueError(
            "Input Missing Error: `signature_algorithm` can only be one of `sha1`, `sha256` or `ed25519`"
        )
    return "~".join(tokens)

Java

To authenticate to Media CDN, set up Application Default Credentials. For more information, see Set up authentication for a local development environment.


import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.util.encoders.Hex;

public class DualToken {

  public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
    // TODO(developer): Replace these variables before running the sample.
    // Secret key as a base64 encoded string.
    byte[] base64Key = new byte[]{};
    // Algorithm can be one of these: SHA1, SHA256, or Ed25519.
    String signatureAlgorithm = "ed25519";
    // (Optional) Start time as a UTC datetime object.
    DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT;
    Optional<Instant> startTime = Optional.empty();
    // Expiration time as a UTC datetime object.
    // If None, an expiration time that's an hour after the current time is used.
    Instant expiresTime = Instant.from(formatter.parse("2022-09-13T12:00:00Z"));

    // ONE OF (`urlPrefix`, `fullPath`, `pathGlobs`) must be included in each input.
    // The URL prefix and protocol to sign.
    // For example: http://example.com/path/ for URLs under /path or http://example.com/path?param=1
    Optional<String> urlPrefix = Optional.empty();
    // A full path to sign, starting with the first '/'.
    // For example: /path/to/content.mp4
    Optional<String> fullPath = Optional.of("http://10.20.30.40/");
    // A set of path glob strings delimited by ',' or '!'.
    // For example: /tv/*!/film/* to sign paths starting with /tv/ or /film/ in any URL.
    Optional<String> pathGlobs = Optional.empty();

    // (Optional) A unique identifier for the session.
    Optional<String> sessionId = Optional.empty();
    // (Optional) Data payload to include in the token.
    Optional<String> data = Optional.empty();
    // (Optional) Header name and value to include in the signed token in name=value format.
    // May be specified more than once.
    // For example: [{'name': 'foo', 'value': 'bar'}, {'name': 'baz', 'value': 'qux'}]
    Optional<List<Header>> headers = Optional.empty();
    // (Optional) A list of comma-separated IP ranges. Both IPv4 and IPv6 ranges are acceptable.
    // For example: "203.0.113.0/24,2001:db8:4a7f:a732/64"
    Optional<String> ipRanges = Optional.empty();

    DualToken.signToken(
        base64Key,
        signatureAlgorithm,
        startTime,
        expiresTime,
        urlPrefix,
        fullPath,
        pathGlobs,
        sessionId,
        data,
        headers,
        ipRanges);
  }

  // Gets the signed URL suffix string for the Media CDN short token URL requests.
  // Result:
  //     The signed URL appended with the query parameters based on the
  // specified URL prefix and configuration.
  public static void signToken(
      byte[] base64Key, String signatureAlgorithm, Optional<Instant> startTime,
      Instant expirationTime, Optional<String> urlPrefix, Optional<String> fullPath,
      Optional<String> pathGlobs, Optional<String> sessionId, Optional<String> data,
      Optional<List<Header>> headers, Optional<String> ipRanges)
      throws NoSuchAlgorithmException, InvalidKeyException {

    String field = "";
    byte[] decodedKey = Base64.getUrlDecoder().decode(base64Key);

    // For most fields, the value in the token and the value to sign
    // are the same. Compared to the token, the FullPath and Headers
    // use a different string for the value to sign. To illustrate this difference,
    // we'll keep the token and the value to be signed separate.
    List<String> tokens = new ArrayList<>();
    List<String> toSign = new ArrayList<>();

    // Check for `fullPath` or `pathGlobs` or `urlPrefix`.
    if (fullPath.isPresent()) {
      tokens.add("FullPath");
      toSign.add(String.format("FullPath=%s", fullPath.get()));
    } else if (pathGlobs.isPresent()) {
      field = String.format("PathGlobs=%s", pathGlobs.get().trim());
      tokens.add(field);
      toSign.add(field);
    } else if (urlPrefix.isPresent()) {
      field = String.format("URLPrefix=%s",
          base64Encoder(urlPrefix.get().getBytes(StandardCharsets.UTF_8)));
      tokens.add(field);
      toSign.add(field);
    } else {
      throw new IllegalArgumentException(
          "User Input Missing: One of `urlPrefix`, `fullPath` or `pathGlobs` must be specified");
    }

    // Check & parse optional params.
    long epochDuration;
    if (startTime.isPresent()) {
      epochDuration = ChronoUnit.SECONDS.between(Instant.EPOCH, startTime.get());
      field = String.format("Starts=%s", epochDuration);
      tokens.add(field);
      toSign.add(field);
    }

    if (expirationTime == null) {
      expirationTime = Instant.now().plus(1, ChronoUnit.HOURS);
    }
    epochDuration = ChronoUnit.SECONDS.between(Instant.EPOCH, expirationTime);
    field = String.format("Expires=%s", epochDuration);
    tokens.add(field);
    toSign.add(field);

    if (sessionId.isPresent()) {
      field = String.format("SessionID=%s", sessionId.get());
      tokens.add(field);
      toSign.add(field);
    }

    if (data.isPresent()) {
      field = String.format("Data=%s", data.get());
      tokens.add(field);
      toSign.add(field);
    }

    if (headers.isPresent()) {
      List<String> headerNames = new ArrayList<>();
      List<String> headerPairs = new ArrayList<>();

      for (Header entry : headers.get()) {
        headerNames.add(entry.getName());
        headerPairs.add(String.format("%s=%s", entry.getName(), entry.getValue()));
      }
      tokens.add(String.format("Headers=%s", String.join(",", headerNames)));
      toSign.add(String.format("Headers=%s", String.join(",", headerPairs)));
    }

    if (ipRanges.isPresent()) {
      field = String.format("IPRanges=%s",
          base64Encoder(ipRanges.get().getBytes(StandardCharsets.US_ASCII)));
      tokens.add(field);
      toSign.add(field);
    }

    // Generate token.
    String toSignJoined = String.join("~", toSign);
    byte[] toSignBytes = toSignJoined.getBytes(StandardCharsets.UTF_8);
    String algorithm = signatureAlgorithm.toLowerCase();

    if (algorithm.equalsIgnoreCase("ed25519")) {
      Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(decodedKey, 0);
      Ed25519Signer signer = new Ed25519Signer();
      signer.init(true, privateKey);
      signer.update(toSignBytes, 0, toSignBytes.length);
      byte[] signature = signer.generateSignature();
      tokens.add(String.format("Signature=%s", base64Encoder(signature)));
    } else if (algorithm.equalsIgnoreCase("sha256")) {
      String sha256 = "HmacSHA256";
      Mac mac = Mac.getInstance(sha256);
      SecretKeySpec secretKeySpec = new SecretKeySpec(decodedKey, sha256);
      mac.init(secretKeySpec);
      byte[] signature = mac.doFinal(toSignBytes);
      tokens.add(String.format("hmac=%s", Hex.toHexString(signature)));
    } else if (algorithm.equalsIgnoreCase("sha1")) {
      String sha1 = "HmacSHA1";
      Mac mac = Mac.getInstance(sha1);
      SecretKeySpec secretKeySpec = new SecretKeySpec(decodedKey, sha1);
      mac.init(secretKeySpec);
      byte[] signature = mac.doFinal(toSignBytes);
      tokens.add(String.format("hmac=%s", Hex.toHexString(signature)));
    } else {
      throw new Error(
          "Input Missing Error: `signatureAlgorithm` can only be one of `sha1`, `sha256` or "
              + "`ed25519`");
    }
    // The signed URL appended with the query parameters based on the
    // specified URL prefix and configuration.
    System.out.println(String.join("~", tokens));
  }

  // Returns a base64-encoded string compatible with Media CDN.
  // Media CDN uses URL-safe base64 encoding and strips off the padding at the
  // end.
  public static String base64Encoder(byte[] value) {
    byte[] encodedBytes = Base64.getUrlEncoder().withoutPadding().encode(value);
    return new String(encodedBytes, StandardCharsets.UTF_8);
  }

  public static class Header {

    private String name;
    private String value;

    public Header(String name, String value) {
      this.name = name;
      this.value = value;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    public String getValue() {
      return value;
    }

    public void setValue(String value) {
      this.value = value;
    }

    @Override
    public String toString() {
      return "Header{"
          + "name='" + name + '\''
          + ", value='" + value + '\''
          + '}';
    }
  }

}

The following sections describe the fields used by tokens.

Required token fields

The following fields are required for every token:

  • Expires
  • One of any of the following:
    • PathGlobs
    • URLPrefix
    • FullPath
  • One of either of the following:
    • Signature
    • hmac

Unless otherwise specified, parameter names and their values are case-sensitive.

The following table explains each parameter:

Field name / aliases Token parameters Signed value

Expires

exp

Integer seconds that have elapsed since the Unix epoch (1970-01-01T00:00:00Z) Expires=EXPIRATION_TIME, after which the token is no longer valid.

PathGlobs

paths, acl

A list of up to five path segments to grant access to. The segments can be delimited by either commas (,) or exclamation marks (!) but not both.

PathGlobs supports wildcarding in paths by using asterisks (*) and question marks (?). A single asterisk (*) character spans any number of path segments, unlike the pattern matching syntax for pathMatchTemplate.

Path parameters, denoted using semicolons (;), are not allowed because they create ambiguity while matching.

For these reasons, ensure that your URL does not contain the following special characters: ,!*?;

PathGlobs=PATHS
URLPrefix

A web-safe base64 encoded URL including the protocol http:// or https:// up to a point of your choosing.

For example, some valid URLPrefix values for `https://example.com/foo/bar.ts` are `https://example.com`, `https://example.com/foo`, and `https://example.com/foo/bar`.

URLPrefix=BASE_64_URL_PREFIX
FullPath None. When specifying FullPath in a token, don't duplicate the path you specified in the signed value. In a token, include the field name without an =. FullPath=FULL_PATH_TO_OBJECT
Signature A web-safe base64 encoded version of the signature. Not applicable
hmac A web-safe base64 encoded version of the HMAC value. Not applicable

PathGlobs wildcard syntax

The following table explains PathGlobs wildcard syntax.

Operator Matches Examples
* (asterisk) Matches zero or more characters in the URL's path, including forward slash (/) characters.
  • /videos/* matches any path that starts with /videos/.
  • /videos/s*/4k/* matches /videos/s/4k/ and /videos/s01/4k/main.m3u8.
  • /manifests/*/4k/* matches /manifests/s01/4k/main.m3u8 and /manifests/s01/e01/4k/main.m3u8. It doesn't match /manifests/4k/main.m3u8.
? (question mark) Matches a single character in the URL's path, not including forward slash (/) characters. /videos/s?main.m3u8 matches /videos/s1main.m3u8. It doesn't match either of /videos/s01main.m3u8 or /videos/s/main.m3u8.

Globs must start with either an asterisk (*) or a forward slash (/) for URL paths.

Because * and /* match all URL paths, we don't recommend using either in your signed tokens. For maximum protection, ensure that your globs match the content that you intend to grant access to.

Optional token fields

Unless otherwise specified, parameter names and their values are case-sensitive.

The following table explains parameter names, any aliases, and details for optional parameters:

Field name / aliases Parameters Signed value

Starts

st

Integer seconds since the Unix epoch (1970-01-01T00:00:00Z) Starts=START_TIME
IPRanges

A list of up to five IPv4 and IPv6 addresses in CIDR format for which this URL is valid in web-safe base64 format. For example, to specify the IP ranges "192.6.13.13/32,193.5.64.135/32", you specify IPRanges=MTkyLjYuMTMuMTMvMzIsMTkzLjUuNjQuMTM1LzMy.

IPRanges may not be helpful to include in tokens when clients are at risk of WAN migrations or cases where the network path to your application frontend is different than the delivery path. Media CDN rejects clients with an HTTP 403 code when they connect with an IP address that is not part of the signed request.

The following are cases that may result in Media CDN rejecting clients with an HTTP 403 code:

  • Dual-stack (IPv4, IPv6) environments
  • Connection migration (WiFi to cellular, and cellular to WiFi)
  • Mobile networks that use Carrier Gateway NAT (CGNAT or CGN)
  • Multi-path TCP (MPTCP)

All of these factors can contribute to a given client having a non-deterministic IP address during a video playback session. If the client IP address changes after you have issued access, and the client attempts to then download a video segment into their playback buffer, they receive an HTTP 403 from Media CDN.

IPRanges=BASE_64_IP_RANGES

SessionID

id

An arbitrary string, useful for logs analysis or playback tracing.

To avoid creating an invalid token, use %-encoded or web-safe base64-encoded strings. The following characters must not be used for SessionID, as they cause the token to be invalid: "~", "&", or " " (space).

SessionID=SESSION_ID_VALUE

Data

data, payload

An arbitrary string, useful for logs analysis.

To avoid creating an invalid token, use %-encoded or web-safe base64-encoded strings. The following characters must not be used for Data, as they cause the token to be invalid: "~", "&", or " " (space).

data=DATA_VALUE
Headers A comma-delimited list of header field names. Header names are case-insensitive for lookups in the request. Header names in the signed values are case-sensitive. If a header is missing, the value is the empty string. If there are multiple copies of a header, they're comma-concatenated. Headers=HEADER_1_NAME=HEADER_1_EXPECTED_VALUE, HEADER_2_NAME=HEADER_2_EXPECTED_VALUE

Examples

The following sections show examples for generating tokens.

Example using FullPath

Consider the following example using the FullPath field:

  • Item requested: http://example.com/tv/my-show/s01/e01/playlist.m3u8
  • Expiration time: 160000000

The signed value is:

Expires=160000000~FullPath=/tv/my-show/s01/e01/playlist.m3u8

To create a token, sign the signed value with either an Ed25519 signature or a symmetric-key HMAC.

The following are example tokens created from a signed value:

Ed25519 signature

Expires=160000000~FullPath~Signature=SIGNATURE_OF_SIGNED_VALUE

Where SIGNATURE_OF_SIGNED_VALUE is the ED25519 signature of the signed value previously created.

Symmetric-key HMAC

Expires=160000000~FullPath~hmac=HMAC_OF_SIGNED_VALUE

Where HMAC_OF_SIGNED_VALUE is the symmetric-key HMAC of the signed value previously created.

In the preceding examples, FullPath is provided in the token, but the value isn't repeated from the path specified in the signed value. This lets you to sign the full path of the request without duplicating the request in the token.

Example using URLPrefix

Consider the following example using the URLPrefix field:

  • Item requested: http://example.com/tv/my-show/s01/e01/playlist.m3u8
  • Expiration time: 160000000

The signed value is:

Expires=160000000~URLPrefix=aHR0cDovL2V4YW1wbGUuY29tL3R2L215LXNob3cvczAxL2UwMS9wbGF5bGlzdC5tM3U4

In the preceding example, we replaced the path to the requested item, http://example.com/tv/my-show/s01/e01/playlist.m3u8 with the path to the item in web-safe Base64 format, aHR0cDovL2V4YW1wbGUuY29tL3R2L215LXNob3cvczAxL2UwMS9wbGF5bGlzdC5tM3U4.

To create a token, sign the signed value with either an Ed25519 signature or a symmetric-key HMAC.

The following are example tokens created from a signed value:

Ed25519 signature

Expires=160000000~URLPrefix=aHR0cDovL2V4YW1wbGUuY29tL3R2L215LXNob3cvczAxL2UwMS9wbGF5bGlzdC5tM3U4~Signature=SIGNATURE_OF_SIGNED_VALUE

Where SIGNATURE_OF_SIGNED_VALUE is the ED25519 signature of the signed value previously created.

Symmetric-key HMAC

Expires=160000000~URLPrefix=aHR0cDovL2V4YW1wbGUuY29tL3R2L215LXNob3cvczAxL2UwMS9wbGF5bGlzdC5tM3U4~hmac=HMAC_OF_SIGNED_VALUE

Where HMAC_OF_SIGNED_VALUE is the symmetric-key HMAC of the signed value previously created.

Example using Headers

Consider the following example using the Headers field:

  • Item requested: http://example.com/tv/my-show/s01/e01/playlist.m3u8
  • Expiration time: 160000000
  • PathGlobs value: *
  • Expected request headers:
    • user-agent: browser
    • accept: text/html

The signed value is:

Expires=160000000~PathGlobs=*~Headers=user-agent=browser,accept=text/html

To create a token, sign the signed value with either an Ed25519 signature or a symmetric-key HMAC.

The following are example tokens created from a signed value:

Ed25519 signature

Expires=160000000~PathGlobs=*~Headers=user-agent,accept~Signature=SIGNATURE_OF_SIGNED_VALUE

Where SIGNATURE_OF_SIGNED_VALUE is the ED25519 signature of the signed value previously created.

Symmetric-key HMAC

Expires=160000000~PathGlobs=*~Headers=user-agent,accept~hmac=HMAC_OF_SIGNED_VALUE

Where HMAC_OF_SIGNED_VALUE is the symmetric-key HMAC of the signed value previously created.

In the preceding examples, Headers=user-agent,accept is provided in the token, but the expected header values aren't repeated from the signed value. This lets you sign specific request header key-value pairs without duplicating the values in the token.