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>