How to generate a secure pre-signed token in GolangJonathan Barney
Jonathan Barney
jonathan.b@aptusai.com
Published on Wed Sep 08 2021

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.

  1. 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

  2. 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