javascriptnode.jsauthenticationcdnbunnycdn

Bunny.net CDN Token authentication not working ( for .m3u8 files)


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',
  ),
);


Solution

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