Securing Amazon Cloudfront stream with signed cookies

In the context of audiobook streaming platform

In one of my recent posts I described the challenges I faced while building the audiobook streaming platform. One of them was securing access to the Cloudfront stream.

There are two ways of securing content that CloudFront delivers: signed URLs and signed cookies. In the case of a segmented file, URL signing doesn't seem to be a way to go. We have to take advantage of signed cookies.

The client browser should have a proper cookie already set while requesting .m3u8 playlist file for each streaming session. You can later refresh this cookie asynchronously. A valid cookie must be attached to the request for each .aac segment file.

Sequence diagram-6.png

When you create a signed cookie, you provide a policy statement that specifies the restrictions on the signed cookie:

  • You can specify the date and time that users can begin to access your content
  • You can specify the date and time that users can no longer access your content
  • You can specify the IP address or range of IP addresses of the users who can access your content

My implementation uses CookieSigner from the official CloudFront SDK for Ruby. You can get it with aws-sdk-cloudfront gem.

module Panel
  class ChaptersController < Panel::PanelController

    def progress
      chapter = Chapter.find_by(user_id: current_user.id, s3_key: params[:s3_key].split('_converted').first)
      chapter.update_columns playback_progress: params[:progress], playback_progress_saved_at: DateTime.now
      sign_cookie(chapter.s3_key)
      head :no_content
    end

    private

    def sign_cookie(s3_key)
      signer = Aws::CloudFront::CookieSigner.new(
        key_pair_id: Rails.application.credentials.aws[:cloudfront_private_key_pair_id],
        private_key: Rails.application.credentials.aws[:cloudfront_private_key]
      )
      signer.signed_cookie(
        nil,
        policy: policy(s3_key, DateTime.now + 1.minute)
      ).each do |key, value|
        cookies[key] = {
          value: value,
          domain: :all
        }
      end
    end

    def policy(s3_key, expiry)
      {
        "Statement" => [
          {
            "Resource" => "#{Rails.application.credentials.aws[:cloudfront_url]}/#{s3_key}*",
            "Condition" => {
              "DateLessThan" => {
                "AWS:EpochTime" => expiry.utc.to_i
              },
              "IpAddress" => {
                "AWS:SourceIp" => "#{request.remote_ip}/32"
              }
            }
          }
        ]
      }.to_json
    end
  end
end