Secure your Stream·续

These values will not be shown again so we recommend saving them securely right away. If you are using Cloudflare Workers, you can store them using Secrets. If you are using another platform, store them in secure environment variables.

You will use these values later to generate the tokens. The pem and jwk fields are base64-encoded, you must decode them before using them (an example of this is shown in step 2).

Step 2: Generate tokens using the key

Once you generate the key in step 1, you can use the pem or jwk values to generate self-signing URLs on your own. Using this method, you do not need to call the Stream API each time you are creating a new token.

Here's an example Cloudflare Worker script which generates tokens that expire in 60 minutes and only work for users accessing the video from UK. In lines 2 and 3, you will configure the id and jwk values from step 1:

// Global variables
const jwkKey = "{PRIVATE-KEY-IN-JWK-FORMAT}";
const keyID = "<KEY_ID>";
const videoUID = "<VIDEO_UID>";
// expiresTimeInS is the expired time in second of the video
const expiresTimeInS = 3600;

// Main function
async function streamSignedUrl() {
    const encoder = new TextEncoder();
    const expiresIn = Math.floor(Date.now() / 1000) + expiresTimeInS;
    const headers = {
        alg: "RS256",
        kid: keyID,
    };
    const data = {
        sub: videoUID,
        kid: keyID,
        exp: expiresIn,
        // Add `downloadable` boolean for access to MP4 or Audio Downloads:
        // downloadable: true,
        accessRules: [
            {
                type: "ip.geoip.country",
                action: "allow",
                country: ["GB"],
            },
            {
                type: "any",
                action: "block",
            },
        ],
    };

const token = `${objectToBase64url(headers)}.${objectToBase64url(data)}`;

const jwk = JSON.parse(atob(jwkKey));

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)}`;

return signedToken;
}

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

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

Step 3: Rendering the video

If you are using the Stream Player, insert the token value returned by the Worker in Step 2 in place of the video id, replacing the entire string located between cloudflarestream.com/ and /iframe:

<iframe
    src="https://customer-<CODE>.cloudflarestream.com/eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ/iframe"
    style="border: none;"
    height="720"
    width="1280"
    allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
    allowfullscreen="true"
></iframe>

If you are using your own player, replace the video id in the manifest url with the token value:

https://customer-<CODE>.cloudflarestream.com/eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ/manifest/video.m3u8

To allow access to MP4 or audio downloads, make sure the video has the download type already enabled. Then add downloadable: true to the payload as shown in the comment above when generating the signed URL. Replace the video id in the download URL with the token value:

  • https://customer-<CODE>.cloudflarestream.com/eyJhbGciOiJ.../downloads/default.mp4

Revoking keys

You can create up to 1,000 keys and rotate them at your convenience. Once revoked all tokens created with that key will be invalidated.

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

# Response:
{
  "result": "Revoked",
  "success": true,
  "errors": [],
  "messages": []
}

Supported Restrictions

Property Name Description
exp Expiration. A unix epoch timestamp after which the token will stop working. Cannot be greater than 24 hours in the future from when the token is signed
nbf Not Before value. A unix epoch timestamp before which the token will not work
downloadable if true, the token can be used to download the mp4 (assuming the video has downloads enabled)
accessRules An array that specifies one or more ip and geo restrictions. accessRules are evaluated first-to-last. If a Rule matches, the associated action is applied and no further rules are evaluated. A token may have at most 5 members in the accessRules array.

accessRules Schema

Each accessRule must include 2 required properties:

  • type: supported values are any, ip.src and ip.geoip.country
  • action: support values are allow and block

Depending on the rule type, accessRules support 2 additional properties:

  • country: an array of 2-letter country codes in ISO 3166-1 Alpha 2 ↗︎ format.
  • ip: an array of ip ranges. It is recommended to include both IPv4 and IPv6 variants in a rule if possible. Having only a single variant in a rule means that rule will ignore the other variant. For example, an IPv4-based rule will never be applicable to a viewer connecting from an IPv6 address. CIDRs should be preferred over specific IP addresses. Some devices, such as mobile, may change their IP over the course of a view. Video Access Control are evaluated continuously while a video is being viewed. As a result, overly strict IP rules may disrupt playback.

Example 1: Block views from a specific country

...
"accessRules": [
    {
        "type": "ip.geoip.country",
        "action": "block",
        "country": ["US", "DE", "MX"],
    },
]

The first rule matches on country, US, DE, and MX here. When that rule matches, the block action will have the token considered invalid. If the first rule doesn't match, there are no further rules to evaluate. The behavior in this situation is to consider the token valid.

Example 2: Allow only views from specific country or IPs

...
"accessRules": [
    {
        "type": "ip.geoip.country",
        "country": ["US", "MX"],
        "action": "allow",
    },
    {
        "type": "ip.src",
        "ip": ["93.184.216.0/24", "2400:cb00::/32"],
        "action": "allow",
    },
    {
        "type": "any",
        "action": "block",
    },
]

The first rule matches on country, US and MX here. When that rule matches, the allow action will have the token considered valid. If it doesn't match we continue evaluating rules

The second rule is an IP rule matching on CIDRs, 93.184.216.0/24 and 2400:cb00::/32. When that rule matches, the allow action will consider the rule valid.

If the first two rules don't match, the final rule of any will match all remaining requests and block those views.

Security considerations

Hotlinking Protection

By default, Stream embed codes can be used on any domain. If needed, you can limit the domains a video can be embedded on from the Stream dashboard.

In the dashboard, you will see a text box by each video labeled Enter allowed origin domains separated by commas. If you click on it, you can list the domains that the Stream embed code should be able to be used on. `

  • *.badtortilla.com covers a.badtortilla.com, a.b.badtortilla.com and does not cover badtortilla.com
  • example.com does not cover www.example.com ↗︎ or any subdomain of example.com
  • localhost requires a port if it is not being served over HTTP on port 80 or over HTTPS on port 443
  • There is no path support - example.com covers example.com/\*

You can also control embed limitation programmatically using the Stream API. uid in the example below refers to the video id.

curl https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/{video_uid} \
--header "Authorization: Bearer <API_TOKEN>" \
--data "{\"uid\": \"<VIDEO_UID>\", \"allowedOrigins\": [\"example.com\"]}"

You can also set allowed origins using the Stream binding:

export default {
    async fetch(request, env) {
        const video = await env.STREAM.video("VIDEO_ID").update({
            allowedOrigins: ["example.com"],
        });
        return Response.json(video);
    },
};
export default {
    async fetch(request, env) {
        const video = await env.STREAM.video("VIDEO_ID").update({
            allowedOrigins: ["example.com"],
        });
        return Response.json(video);
    },
};

Allowed Origins

The Allowed Origins feature lets you specify which origins are allowed for playback. This feature works even if you are using your own video player. When using your own video player, Allowed Origins restricts which domain the HLS/DASH manifests and the video segments can be requested from.

Signed URLs

Combining signed URLs with embedding restrictions allows you to strongly control how your videos are viewed. This lets you serve only trusted users while preventing the signed URL from being hosted on an unknown site.

Was this helpful?

YesNo

On this page

Secure your Stream·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/#page","headline":"Secure your Stream","description":"Restrict access to Cloudflare Stream videos using signed URLs and tokens.","url":"https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/","inLanguage":"en","image":"https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/og.png?v=de718c2fda3a6680","dateModified":"2026-05-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: Play Cloudflare Stream videos with any HLS/DASH-compatible player on web, iOS, or Android. title: Use your own player image: https://developers.cloudflare.com/stream/viewing-videos/using-own-player/og.png?v=7f7bfaf5f1d9fbb9


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/stream/llms.txt
Use this file to discover all available pages before exploring further.