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