Generating Tokens

Generating Tokens #

Do not create signed URLs in or send your signing key to client-side code. This page is for experimenting. Creating Signed URLs should be done in a Worker or server-side application to protect the signing key.

Step 1: Get a signing Token #

Provision a signing token from Stream to create signed URLs in your app or server application.

curl --request POST \
  "https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/keys" \
  --header "Authorization: Bearer <API_TOKEN>"

The response will include the key’s ID as well as the key in PEM and JWK format. (The pem and jwk properties here have been truncated; they will be long.)

{
  "result": {
    "id": "8f926b2b01f383510025a78a4dcbf6a",
    "pem": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQk...",
    "jwk": "eyJ1c2UiOiJzaWciLCJrdHkiOiJSU0EiLCJraWQiOiI4ZjkyNmIyYj...",
    "created": "2021-06-15T21:06:54.763937286Z"
  },
  "success": true,
  "errors": [],
  "messages": []
}

Step 2: Creating a Signed URL #

Signed URLs are just signed JWT payloads. When creating a Signed URL, specify the video, any access restrictions, and effective time ranges, and sign the token with the key. This can be done in several ways, but here’s one way.

Key ID
Key JWK
Paste your entire jwk property from the keys endpoint. It will not leave this browser. Never provide your JWK to client-side code.
Video ID
Will be used in the JWT as the "Subject" (sub)
Expiration
Provided as a Unix/epoch timestamp
Access Rules
If provided, must be a JSON payload.
Valid? Yes

Step 3: Result #

The resulting JWT is used in place of the Video ID when creating URLs:

Use the above to generate a link like https://cloudflarestream.com/<TOKEN>/watch, like this:


<script>
  const generateSignedURL = async () => {
    const encoder = new TextEncoder();

    const headers = {
      "alg": "RS256",
      "kid": kidEl.value,
    };

    const data = {
      "sub": subEl.value,
      "kid": kidEl.value,
      "exp": expEl.value,
    };

    if (accessRulesEl.value && validateAccessRules()) {
      data.accessRules = JSON.parse(accessRulesEl.value);
    };

    const token = `${objectToBase64url(headers)}.${objectToBase64url(data)}`;
    const jwk = JSON.parse(atob(jwkEl.value));

    const key = await crypto.subtle.importKey(
      "jwk", jwk,
      {
        name: 'RSASSA-PKCS1-v1_5',
        hash: 'SHA-256',
      },
      false, [ "sign" ]
    );

    const signature = await crypto.subtle.sign(
      { name: 'RSASSA-PKCS1-v1_5' }, key,
      encoder.encode(token)
    );

    const signedToken = `${token}.${arrayBufferToBase64Url(signature)}`;

    outputEl.value = signedToken;

    outputLink.href = `https://cloudflarestream.com/${signedToken}/watch`;
    outputLink.innerText = 'Video Link';
  };

  // Utilities functions
  const arrayBufferToBase64Url = (buffer) => {
    return btoa(String.fromCharCode(...new Uint8Array(buffer)))
      .replace(/=/g, '')
      .replace(/\+/g, '-')
      .replace(/\//g, '_');
  };

  const objectToBase64url = (payload) => {
    return arrayBufferToBase64Url(
      new TextEncoder().encode(JSON.stringify(payload)),
    );
  };

</script>