I am currently implementing a video streaming solution that utilizes the HTTP Live Streaming (HLS). As part of the token generation process, I am utilizing a secret key obtained from the "Token Authentication Key" section under the "Stream > Security > Token Authentication Key" in our CDN service management interface. Using this secret key, I am signing the HLS Playlist URLs in a Node.js environment.
However, despite following the prescribed procedures for URL signing, I am encountering an HTTP 403 Forbidden error when attempting to access the HLS playlist URLs (.m3u8).
Interestingly, when I apply the same signing process to the URLs of video thumbnail files (e.g., thumbnail.jpg), I am able to access the files without issue. This successful authentication with the thumbnail URLs validates the functionality of the token generation and authentication process to some extent.
I tried
Following are codes that works for thumbnail.jpg but not for playlist.m3u8:
var crypto = require('crypto'),
securityKey = 'key_from_stream_security',
path = '/a1640499-52ef-4721-9ba9-4e659cada6f5/playlist.m3u8';
// Set the time of expiry to 4 days from now
var expires = 1688885053;
var hashableBase = securityKey + path + expires;
// 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://vz-9ac10aa0-92d.b-cdn.net' +
path +
'?token=' +
token +
'&expires=' +
expires;
console.log(url);
and
var queryString = require('querystring');
var crypto = require('crypto');
var URL = require('url').URL; // require URL constructor
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 = 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);
}
console.log(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
);
}
}
console.log(
signUrl(
'https://vz-9ac10aa0-92d.b-cdn.net/548789ea-d0d7-4dac-b656-cc2acedf0f5c/playlist.m3u8',
'key_from_stream_security',
1688885053,
'',
false,
'/548789ea-d0d7-4dac-b656-cc2acedf0f5c/playlist.m3u8',
),
);
The issue was in pathAllowed and isDirectory
I needed to make the call like this from second code:
signUrl(
'https://vz-9ac10aa0-92d.b-cdn.net/548789ea-d0d7-4dac-b656-cc2acedf0f5c/playlist.m3u8',
'token_authentication_from_stream_security',
1688885053,
'',
true,
'/548789ea-d0d7-4dac-b656-cc2acedf0f5c/',
),
The HLS url is a .m3u8 file that just contains the chunk reference. When we try to use .m3u8 url in player to stream video, it accesses all the chunks. So besides signing the .m3u8 file, we needed to sign the directory where .m3u8 is placed so the token also works for chunks too.