Signed URLs with Ruby

Posted on 2023-01-16 by Karol Bąk Comments


Signed URLs can be a very useful solution in many cases when you need to provide limited access to some resources or actions. Today I’ll focus on when and how to use them in Ruby, with Rails, or by providing a custom implementation.

About Signed URLs

Signed URLs, as the name suggests, contain signatures that allow us to validate if they were generated by a trusted source. What is more, they may expire over time.

They can be used in many cases:

  • account confirmations, password change confirmations, etc. without storing any tokens in DB
  • providing access to resources for not authenticated users (for example, users in the app can generate some reports and share them with anyone with a link)
  • providing access to resources in other apps without shared authentication (example: S3 direct upload) - in this case both services need to use the same secret to sign and verify URLs. What is important, your app doesn’t need to make any calls to API to generate such URLs.

How it works?

Signed URL apart from a standard query usually contains at least two additional params - signature and expires_at. To generate such URL we have to calculate the signature using the rest of the params.



Signature is calculated based on resource type - reports, id - 50, and expiry time - 1673620751.

To validate if the URL is valid we generate the signature again and compare it with the one in params.

In another approach (for example in the one implemented in Rails) you can merge all these things into one token. Example:


Th first part of this token (before --) contains information about resource and expiry time:

=> "reports-50-1673620920"

The second part is just a signature.

Now let’s see how can we generate and validate such URLs in ruby.

Rails Signed ID

If you are using Rails and need to generate a signed URL for one of your models, the easiest way is to use Signed ID (introduced in 6.1).

signed_id = Report.find(50).signed_id(expires_in: 3.hours)
url = "/reports/share/#{signed_id}"

To find a resource and validate the signature:

=> #<Report id: 50... 
# now we can allow user to view report

=> nil
# signature is invalid, don't allow user to see the report

You can also pass purpose to signed_id method if you generate signed URLs for different actions on your model (for example password_reset and account_confirmation on User).


Another approach in Rails (or any other app using ActiveSupport) is MessageVerifier. It’s useful if your resource is not a model or you are not using ActiveRecord.

verifier =['MY_SECRET_KEY'])
signed_msg = verifier.generate("reports-50", expires_in: 3.hours)
url = "/reports/share/#{signed_msg}"

Signature validation:

=> "reports-50"

=> nil

=> "reports-50"

=> ActiveSupport::MessageVerifier::InvalidSignature

From the scratch

If you are not using ActiveSupport or for some reason you have to implement it from scratch it’s pretty easy. To keep it simple let’s implement a case when signature is a separate param from resource id and expires_at (/reports/50?signature=...&expires_at=...).

class MessageSigner
  def initialize(key = ENV['MY_SECRET_KEY'])
    @key = key

  def sign(message, expires_at:)
    OpenSSL::HMAC.hexdigest('SHA256', @key, "#{message}--#{expires_at}") # calculate signature

  def verify(message, signature, expires_at:)
    # check if signature is still valid, 
    # you can trust expires_at params since if it was modified by user it'll fail on signature validation
    return false if >= expires_at

    expected_signature = sign(message, expires_at: expires_at) # recalculate signature
    Rack::Utils.secure_compare(signature, expected_signature) # use secure_compare to avoid timing attacks

Creating signature:

signer =
expires_at = + 60*60*3
signature = signer.sign("reports-50", expires_at: expires_at)
url = "/reports/50?signature=#{signature}&expires_at=#{expires_at}"

Validating signature:

signer =
signer.verify("reports-#{params[:id]}", params[:signature], expires_at: params[:expires_at].to_i)
# allow user to see report if verify method returns true

To implement behavior like in Rails all you have to do is merge message and expires_at (manually to one string or with some serializer like Marshal) and encode it with Base64. You can consider it as your homework :)


As you can see generating and validating signed URLs in Ruby is really simple with or without Rails. It can be used for many purposes as an alternative to other solutions (temporary tokens in DB - ugh!).