The Problem
Recently I had to configure an API already built in Golang to work with the Cloudflare Stream service, in order to provide links for HLS streaming of video. The actual copying into and sharing of videos from Cloudflare Stream is remarkably simple, but when you need to add a security layer on top to ensure that the videos aren't accessible to just anyone it gets a bit more complicated.
Investigation
Cloudflare provides a way to limit access to videos,documented here, by setting the requireSignedURLS
flag to true. Once this flag is enabled any request to stream this video will have to authenticate with a signed token.
curl -X POST -H "Authorization: Bearer $TOKEN" "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT/stream/$VIDEOID" -H "Content-Type: application/json" -d "{\"uid\": \"$VIDEOID\", \"requireSignedURLs\": true }"
Now in order to stream this video, you will need a signed token. These can be retrieved one of two ways.
Cloudflare provides a handy endpoint to generate these tokens for us, such as seen here. Just call this endpoint then return the generated token to the user which they can then use to access the video stream
Cloudflare Stream allows us to generate our own tokens, which will save us the hassle of having to call their service every time we need one.
Local token generation is the much better, albeit more complicated option, as it saves us the network costs and time wasted of making a request to the Cloudflare Stream API for every video we want to provide a stream link for.
The Solution
The Cloudflare Stream Documentation provides an example of how to generate the token ourselves in Javascript, but it uses several functions that are unique to Javascipt and somewhat difficult to transfer over to a different language.
After looking around for a bit, I couldn't find any other Golang library for Cloudflare Stream that replicated this functionality, so I determined I would just write it myself since it was simple enough.
The following is the result of my efforts to translate the Cloudflare provided Javascript code over to Golang, and it has worked great to generate the tokens in much less time than it would have taken to request them from the API.
// Developed by Aptus Engineering, Inc. <https://aptusai.com>
package cloudflare
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"os"
"time"
)
func GenerateStreamToken(videoID string) (token string, err error) {
// Define constants of key pem and id, as well as token valididty length
rsaKey := os.Getenv("CSTREAM_PEM")
keyID := os.Getenv("CSTREAM_ID")
validityLength := time.Hour
// Calculate expiry time
expiry := time.Now().Add(validityLength)
// Decode the b64 encoded environment variable provided from cloudflare
pemBytes, err := base64.StdEncoding.DecodeString(rsaKey)
if err != nil {
return token, err
}
// Grab just the pemdata from the decoded bytes
pemData, _ := pem.Decode(pemBytes)
// Parse the pem into a x509 private key
privateKeyImported, err := x509.ParsePKCS1PrivateKey(pemData.Bytes)
if err != nil {
return token, err
}
// Define signature headers
headers := struct {
Alg string `json:"alg"`
Kid string `json:"kid"`
}{"RS256", keyID}
// Define Token parameters. Add desired additional security rules here
data := struct {
Sub string `json:"sub"`
Kid string `json:"kid"`
Exp int64 `json:"exp"`
AccessRules []interface{} `json:"access_rules"`
}{videoID, keyID, expiry.Unix(), []interface{}{}}
// Convert the token headers and data to a b64 string, the concatenate it
hds, err := objectToBase64URL(headers)
if err != nil {
return token, err
}
dt, err := objectToBase64URL(data)
if err != nil {
return token, err
}
tkMessage := hds + "." + dt
// Generate the hash of the message(combined token headers and data)
hashed := sha256.Sum256([]byte(tkMessage))
// Sign with the private key that we have parsed
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKeyImported, crypto.SHA256, hashed[:])
// Concatenate signature onto the data itself, and return
token = tkMessage + "." + base64.RawURLEncoding.EncodeToString(signature)
return token, err
}
// Handles the JSON marshal and base64 encoding in format neccesary for cloudflare spec
func objectToBase64URL(v interface{}) (string, error) {
res, err := json.Marshal(&v)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(res), nil
}
To add additional security restrictions you can add to the AccessRules section of the token data.
This example reads in the key and key id from environment variables to strings, but they could just as easily be loaded from somewhere else.
Want to join our team? We are always looking to add creative, problem solvers to our team! If you're interested, please contact us