BunnyCDN provides a powerful token authentication system to strictly control who, where and for how long can access your content.
This guide contains the documentation on how to enable, configure and generate the tokens to securely access your content. If you are looking for our older, but much more simple token authentication system, please check older token authentication guide instead. Both can be used interchangeably and our system will automatically detect and validate both token types.
What is token authentication?
First of all, what is token authentication? In short, if enabled, token authentication will block all requests to your URLs unless a valid token is passed with the request. The token is then compared on our server and we check if both values match. If the hash we generate matches the one sent by the request, then the request is accepted, otherwise, a 403 page is returned.
The token can then either be put in as a query parameter or used as part of the URL path. The path version is useful for situations such as video delivery. Here are two examples of signed URLs:
https://test.b-cdn.net/my-partial/url/video.mp4?token=aiDhHXZGHidbDZZqIY14z1eHhhbZvMlzh_7u9cEIdfI&token_path=%2Fmy-partial%2Furl%2F&expires=1598024587
https://test.b-cdn.net/bcdn_token=aiDhHXZGHidbDZZqIY14z1eHhhbZvMlzh_7u9cEIdfI&expires=1598024587&token_path=%2Fmy-partial%2Furl%2F/my-partial/url/video.mp4
How to sign an URL - Part 1: Parameters
This section contains instructions on how to generate and format the unique tokens and use those to sign an URL. We also provide code examples and helper functions for popular programming languages that allow you to sign an URL with a simple function call.
The signing process consists of the following parameters that need to be added to the URL:
- token (required)
- expires (required)
- token_path (optional)
- token_countries (optional)
- token_countries_blocked (optional)
token
The token parameter is the main key in signing the URL. It represents a Base64 encoded SHA256 hash based on the URL, expiration time and other parameters. We will show how to generate the token in the next section.
expires
The expires parameter is the second main parameter that must always be included. It allows you to control exactly for how long the signed URL is valid. It is a UNIX timestamp based time representation. Any request after this timestamp will be rejected.
For example, a token expiring on 08/21/2020 @ 3:43 pm (UTC) would use:
&expires=1598024587
token_path (optional)
By default, the full request URL is used for signing a request. The token_path parameter allows you to specify a partial path that will be used instead of the full URL.
Let's use this URL for an example:
https://test.b-cdn.net/my-partial/url/playlist.m3u8
By default, the full path would need to be used when generating the token, and only this specific path would be allowed to be accessed with the specific token:
/my-partial/url/playlist.m3u8
However, we by passing the token_path for:
&token_path=%2Fmy-partial%2Furl%2F
That would create a token that has access to any file within that path, for example:
https://test.b-cdn.net/my-partial/url/playlist.m3u8
https://test.b-cdn.net/my-partial/url/file1.ts
https://test.b-cdn.net/my-partial/url/file2.ts
https://test.b-cdn.net/my-partial/url/file3.ts
Would all be covered by the token. This is useful for video delivery.
token_countries (optional)
The token_countries allows you to specify a list of countries that will have access to the URL. Any request outside of these countries will be rejected. It is a comma-separated list, for example:
&token_countries=SI,GB
token_countries_blocked (optional)
The token_countries_blocked is similar to token_countries. It allows you to specify a list of countries that will not have access to the URL. Any request from one of the listed countries will be rejected. It is a comma-separated list, for example:
&token_countries_blocked=SI,GB
How to sign a URL - Part 2: Generating the token
Now that we understand all the parameters, we can proceed to actually generate the token. The token is a Base64 encoded raw SHA256 hash based on the signed URL, expiration time, and any extra parameters. To generate the token, you can use the following algorithm:
Base64Encode(
SHA256_RAW(token_security_key + signed_url + expiration + (optional)remote_ip + (optional)encoded_query_parameters)
)When generating the token, all of the URL query parameters must also be appended at the end of the hashable string in an ascending order except for the "token" and "expires" parameter which should not be included. These must NOT be URL encoded, but they need to be in the form-encoded POST format without the starting "?" character, for example:
"param1=something¶m2=something¶m3=something"
An example hashable base for a SHA256 token would then be:
security-key/my-directory/12345192.168.1.1token_countries=SI,GB&width=500&token_path=/my-directory/
To properly format the token you have to then replace the following characters in the resulting Base64 string: '\n' with '', '+' with '-', '/' with '_' and '=' with ''.
How to sign an URL - Part 3: Putting it all together
Once we have the token and all the parameters, it's time to put it all together. There are currently two ways of signing the URL.
Query Parameter Based Tokens
The easiest way is to append the token to the request using query parameters, for example:
https://test.b-cdn.net/my-partial/url/video.mp4?token=aiDhHXZGHidbDZZqIY14z1eHhhbZvMlzh_7u9cEIdfI&token_path=%2Fmy-partial%2Furl%2F&expires=1598024587
URL Path Based Tokens
The second option are the URL path based tokens that are useful for video delivery because any sub-request to the same directory of the URL will already have the token included.
https://test.b-cdn.net/bcdn_token=aiDhHXZGHidbDZZqIY14z1eHhhbZvMlzh_7u9cEIdfI&expires=1598024587&token_path=%2Fmy-partial%2Furl%2F/my-partial/url/video.mp4
Note that when generating the token, the token_path parameter does not need to include the signature path.
If it all went well, you should now have secure URLs that are only accessible through the signed URLs. If you experience any difficulties generating the tokens, please reach out to support@bunny.net, and we'll be happy to help with the implementation.
Examples
→ Bash
#!/bin/bash
# Set variables
URL="YOUR-DEFAULT-URL"
SECURITY_KEY="TOKEN-KEY-SIGN"
EXPIRATION_TIME=3600
USER_IP=""
IS_DIRECTORY="false"
PATH_ALLOWED=""
COUNTRIES_ALLOWED=""
COUNTRIES_BLOCKED=""
# Helper function to add the countries_allowed/countries_blocked parameters if necessary.
add_countries() {
local url=$1
local allowed=$2
local blocked=$3
if [[ -n "$allowed" ]]; then
if [[ "$url" =~ \? ]]; then
url+="&token_countries=$allowed"
else
url+="?token_countries=$allowed"
fi
fi
if [[ -n "$blocked" ]]; then
if [[ "$url" =~ \? ]]; then
url+="&token_countries_blocked=$blocked"
else
url+="?token_countries_blocked=$blocked"
fi
fi
echo "$url"
}
# Function to URL-encode a string
url_encode() {
local string="$1"
local encoded=""
encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$string'''))")
echo "$encoded"
}
# Function to generate URL Authentication
sign_url() {
local url=$1
local security_key=$2
local EXPIRATION_TIME=${3:-3600}
local user_ip=$4
local is_directory=$5
local path_allowed=$6
local countries_allowed=$7
local countries_blocked=$8
local parameter_data=""
local parameter_data_url=""
local expires=$(($(date +%s) + EXPIRATION_TIME))
url=$(add_countries "$url" "$countries_allowed" "$countries_blocked")
local parsed_url=$(echo "$url" | awk -F'[?]' '{print $1}')
local query_string=$(echo "$url" | awk -F'[?]' '{print $2}')
local scheme=$(echo "$parsed_url" | awk -F'://' '{print $1}')
local netloc=$(echo "$parsed_url" | awk -F'://' '{print $2}' | awk -F'/' '{print $1}')
local path=$(echo "$parsed_url" | awk -F"$netloc" '{print $2}')
local parameters=$(echo "$query_string" | tr '&' '\n' | sort)
local signature_path=""
local signature_ip=""
if [[ -n "$path_allowed" ]]; then
signature_path="$path_allowed"
parameters+="&token_path=$signature_path"
else
signature_path="$path"
fi
for param in $parameters; do
key=$(echo $param | cut -d= -f1)
value=$(echo $param | cut -d= -f2)
if [[ -n "$value" ]]; then
parameter_data+="$key=$value&"
parameter_data_url+="&$key=$(url_encode "$value")"
fi
done
parameter_data="${parameter_data%&}"
if [[ -n "$user_ip" ]]; then
signature_ip="$user_ip"
fi
hashable_base="$security_key$signature_path$expires$signature_ip$parameter_data"
token=$(echo -n "$hashable_base" | openssl dgst -sha256 -binary | base64)
token=$(echo "$token" | tr '+/' '-_' | tr -d '=')
if [[ "$is_directory" == "true" ]]; then
echo "$scheme://$netloc/bcdn_token=$token$parameter_data_url&expires=$expires$signature_path"
else
echo "$scheme://$netloc$signature_path?token=$token$parameter_data_url&expires=$expires"
fi
}
# Generate the signed URL
signed_url=$(sign_url "$URL" "$SECURITY_KEY" "$EXPIRATION_TIME" "$USER_IP" "$IS_DIRECTORY" "$PATH_ALLOWED" "$COUNTRIES_ALLOWED" "$COUNTRIES_BLOCKED")
echo "Signed URL: $signed_url"
→ Node.js
Simple:
var crypto = require('crypto'),
securityKey = '229248f0-f007-4bf9-ba1f-bbf1b4ad9d40',
path = '/300kb.jpg';
// Set the time of expiry to one hour from now
var expires = Math.round(Date.now() / 1000) + 3600;
var hashableBase = securityKey + path + expires;
// If using IP validation
// hashableBase += "146.14.19.7";
// Generate and encode the token
var md5String = crypto.createHash("md5").update(hashableBase).digest("binary");
var token = new Buffer(md5String, 'binary').toString('base64');
token = token.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');
// Generate the URL
var url = 'https://token-tester.b-cdn.net' + path + '?token=' + token + '&expires=' + expires;
console.log(url)
Advanced:
token.js
var queryString = require("querystring");
var crypto = require("crypto");
function addCountries(url, a, b) {
var tempUrl = url;
if (a != null) {
var tempUrlOne = new URL(tempUrl);
tempUrl += ((tempUrlOne.search == "") ? "?" : "&") + "token_countries=" + a;
}
if (b != null) {
var tempUrlTwo = new URL(tempUrl);
tempUrl += ((tempUrlTwo.search == "") ? "?" : "&") + "token_countries_blocked=" + b;
}
return tempUrl;
}
function signUrl(url, securityKey, expirationTime = 3600, userIp, isDirectory = false, pathAllowed, countriesAllowed, countriesBlocked) {
/*
url: CDN URL w/o the trailing '/' - exp. http://test.b-cdn.net/file.png
securityKey: Security token found in your pull zone
expirationTime: Authentication validity (default. 86400 sec/24 hrs)
userIp: Optional parameter if you have the User IP feature enabled
isDirectory: Optional parameter - "true" returns a URL separated by forward slashes (exp. (domain)/bcdn_token=...)
pathAllowed: Directory to authenticate (exp. /path/to/images)
countriesAllowed: List of countries allowed (exp. CA, US, TH)
countriesBlocked: List of countries blocked (exp. CA, US, TH)
*/
var parameterData = "", parameterDataUrl = "", signaturePath = "", hashableBase = "", token = "";
var expires = Math.floor(new Date() / 1000) + expirationTime;
var url = addCountries(url, countriesAllowed, countriesBlocked);
var parsedUrl = new URL(url);
var parameters = (new URL(url)).searchParams;
if (pathAllowed != "") {
signaturePath = pathAllowed;
parameters.set("token_path", signaturePath);
} else {
signaturePath = decodeURIComponent(parsedUrl.pathname);
}
parameters.sort();
if (Array.from(parameters).length > 0) {
parameters.forEach(function(value, key) {
if (value == "") {
return;
}
if (parameterData.length > 0) {
parameterData += "&";
}
parameterData += key + "=" + value;
parameterDataUrl += "&" + key + "=" + queryString.escape(value);
});
}
hashableBase = securityKey + signaturePath + expires + ((userIp != null) ? userIp : "") + parameterData;
token = Buffer.from(crypto.createHash("sha256").update(hashableBase).digest()).toString("base64");
token = token.replace(/\n/g, "").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
if (isDirectory) {
return parsedUrl.protocol+ "//" + parsedUrl.host + "/bcdn_token=" + token + parameterDataUrl + "&expires=" + expires + parsedUrl.pathname;
} else {
return parsedUrl.protocol + "//" + parsedUrl.host + parsedUrl.pathname + "?token=" + token + parameterDataUrl + "&expires=" + expires;
}
}
module.exports = {signUrl};
index.js
// Import the signUrl function from the "token.js" file
const {signUrl} = require("./token");
// securityKey can be found on the "Security" tab of the pull zone in the dashboard
const securityKey = "229248f0-f007-4bf9-ba1f-bbf1b4ad9d40";
// The URL would be the full URL you are signing.
const url = "https://token-tester.b-cdn.net/300kb.jpg";
// The expiration time is set to one hour by default.
const expires = Math.floor(Date.now() / 1000) + 3600;
// The IP of the user, leave blank if not using.
const ip = "";
// Enable or disable the use of path tokens.
const pathTokenEnabled = false
// If pathTokenEnabled is enabled, supply the path to the current directory.
const pathAllowedRoute = "/";
// List of countries allowed seperated by a comma e.g. gb,se,jp
const countriesAllowed = "";
// List of countries blocked seperated by a comma e.g. gb,se,jp
const countriesBlocked = "";
// Generate signed URL.
console.log(signUrl(url, securityKey, expires, ip, pathTokenEnabled, pathAllowedRoute, countriesAllowed, countriesBlocked));→ Python
import urllib.parse, time, hashlib, base64
VERSION = "2"
def add_countries(url, a, b):
"""
Helper to add the countries_allowed/countries_blocked
parameters if necessary.
a: List of countries allowed (exp. CA, US, TH)
b: List of countries blocked (exp. CA, US, TH)
"""
allowed, blocked = "", ""
if a:
url += {1: "?", 0: "&"}[urllib.parse.urlparse(url).query == ""] + "token_countries=" + a
if b:
url += {1: "?", 0: "&"}[urllib.parse.urlparse(url).query == ""] + "token_countries_blocked=" + b
return url;
def sign_url(url, security_key, expiration_time = 86400, user_ip = "", isDirectory = True, path_allowed = "", countries_allowed = "", countries_blocked = ""):
"""
Generates URL Authentication Beacon
url: CDN URL w/o the trailing '/' - exp. http://test.b-cdn.net/file.png
security_key: Security token found in your pull zone
expiration_time: Authentication validity (default. 86400 sec/24 hrs)
user_ip: Optional parameter if you have the User IP feature enabled
countries_allowed: List of countries allowed (exp. CA, US, TH)
countries_blocked: List of countries blocked (exp. CA, US, TH)
"""
parameter_data, parameter_data_url = "", ""
expires = str(int(time.time() + expiration_time))
url = add_countries(url, countries_allowed, countries_blocked)
parsed_url = urllib.parse.urlparse(url)
parameters = urllib.parse.parse_qs(parsed_url.query)
if path_allowed:
signature_path = path_allowed
parameters["token_path"] = signature_path
else:
signature_path = parsed_url.path
parameters = dict((a, parameters[a]) for a in sorted(parameters))
if parameters:
for value in parameters:
if len(parameter_data) > 0:
parameter_data += "&"
parameter_data_url += "&"
parameter_data += value + "=" + "".join(parameters[value])
parameter_data_url += value + "=" + urllib.parse.quote("".join(parameters[value]), safe="")
hashable_base = security_key + signature_path + expires + parameter_data + {1: user_ip, 0: ""}[user_ip != None]
token = base64.b64encode(hashlib.sha256(str.encode(hashable_base)).digest())
token = token.decode().replace("\n", "").replace("+", "-").replace("/", "_").replace("=", "")
if isDirectory:
return parsed_url.scheme + "://" + parsed_url.netloc + "/bcdn_token=" + token + parameter_data_url + "&expires=" + str(expires) + parsed_url.path
else:
return parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?token=" + token + parameter_data_url + "&expires=" + str(expires)→ Go
package main
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"sort"
"strings"
"time"
)
func addCountries(urlStr string, a string, b string) string {
tempURL, _ := url.Parse(urlStr)
if a != "" {
urlValues := tempURL.Query()
urlValues.Add("token_countries", a)
tempURL.RawQuery = urlValues.Encode()
}
if b != "" {
urlValues := tempURL.Query()
urlValues.Add("token_countries_blocked", b)
tempURL.RawQuery = urlValues.Encode()
}
return tempURL.String()
}
func signURL(urlStr string, securityKey string, expirationTime int64, userIP string, pathAllowed string, countriesAllowed string, countriesBlocked string) string {
expires := time.Now().Unix() + expirationTime
urlStr = addCountries(urlStr, countriesAllowed, countriesBlocked)
parsedURL, _ := url.Parse(urlStr)
parameters := parsedURL.Query()
var signaturePath string
if pathAllowed != "" {
signaturePath = pathAllowed
parameters.Set("token_path", signaturePath)
} else {
signaturePath, _ = url.QueryUnescape(parsedURL.Path)
}
// Manually sort the parameters
var keys []string
for key := range parameters {
keys = append(keys, key)
}
sort.Strings(keys)
var parameterData string
var parameterDataURL string
for _, key := range keys {
values := parameters[key]
sort.Strings(values)
for _, value := range values {
if value == "" {
continue
}
if parameterData != "" {
parameterData += "&"
}
parameterData += key + "=" + value
parameterDataURL += "&" + key + "=" + url.QueryEscape(value)
}
}
hashableBase := securityKey + signaturePath + fmt.Sprint(expires) + userIP + parameterData
hash := sha256.New()
hash.Write([]byte(hashableBase))
token := base64.StdEncoding.EncodeToString(hash.Sum(nil))
token = strings.ReplaceAll(token, "\n", "")
token = strings.ReplaceAll(token, "+", "-")
token = strings.ReplaceAll(token, "/", "_")
token = strings.ReplaceAll(token, "=", "")
isDirectory := pathAllowed != ""
if isDirectory {
return parsedURL.Scheme + "://" + parsedURL.Host + "/bcdn_token=" + token + parameterDataURL + "&expires=" + fmt.Sprint(expires) + parsedURL.Path
} else {
return parsedURL.Scheme + "://" + parsedURL.Host + parsedURL.Path + "?token=" + token + parameterDataURL + "&expires=" + fmt.Sprint(expires)
}
}
func main() {
// Example usage:
signedURL := "https://token-tester.b-cdn.net/300kb.jpg"
securityKey := "229248f0-f007-4bf9-ba1f-bbf1b4ad9d40"
// Time until the generated URL expires (in seconds, e.g., 3600 for 1 hour)
expirationTime := int64(3600)
// Optional: The user's IPv4 address; leave empty if not using this feature
userIP := ""
// Optional: Specify a path allowed for authentication (e.g., "/video-id" for HLS playlists)
pathAllowedRoute := ""
// Optional: List of allowed countries (ISO codes separated by commas)
countriesAllowed := ""
countriesBlocked := ""
// Generate the signed URL based on the provided parameters
result := signURL(signedURL, securityKey, expirationTime, userIP, pathAllowedRoute, countriesAllowed, countriesBlocked)
// Print the generated signed URL
fmt.Println(result)
}