Giter Club home page Giter Club logo

url_signature's Introduction

URL Signature

Create and verify signed urls. Supports expiration time.

Tests Code Climate Version Downloads

Installation

gem install url_signature

Or add the following line to your project's Gemfile:

gem "url_signature"

Usage

To create a signed url, you can use SignedURL.call(url, **kwargs), where arguments are:

  • key: The secret key that will be used to generate the HMAC digest.
  • params: Any additional params you want to add as query strings.
  • expires: Any integer representing an epoch time. Urls won't be verified after this date. By default, urls don't expire.
  • hmac_proc: Proc that will generate the signature. By default, it generates a base64url(sha512_hmac(data)) signature (with no padding). The proc will be called with two parameters: key and data.
  • signature_param: The signature's param name. By default it's signature.
  • expires_param: The expires' param name. By default it's expires.
key = "secret"

signed_url = SignedURL.call("https://nandovieira.com", key: key)
#=> "https://nandovieira.com/?signature=87fdf44a5109c54edff2e0258b354e32ba5b..."

You can use the method SignedURL.verified?(url, **kwargs) to verify if a signed url is valid.

key = "secret"

signed_url = SignedURL.call("https://nandovieira.com", key: key)

SignedURL.verified?(signed_url, key: key)
#=> true

Alternatively, you can use SignedURL.verify!(url, **kwargs), which will raise exceptions if a url cannot be verified (e.g. has been tampered, it's not fresh, or is a plain invalid url).

  • URLSignature::InvalidURL if url is not valid
  • URLSignature::ExpiredURL if url has expired
  • URLSignature::InvalidSignature if the signature cannot be verified

To create a url that's valid for a time window, use :expires. The following example create a url that's valid for 2 minutes.

key = "secret"

signed_url = SignedURL.call(
  "https://nandovieira.com",
  key: secret,
  expires: Time.now.to_i + 120
)
#=> "https://nandovieira.com/?expires=1604477596&signature=7ac5eaee20d316..."

Maintainer

Contributors

Contributing

For more details about how to contribute, please read https://github.com/fnando/url_signature/blob/main/CONTRIBUTING.md.

License

The gem is available as open source under the terms of the MIT License. A copy of the license can be found at https://github.com/fnando/url_signature/blob/main/LICENSE.md.

Code of Conduct

Everyone interacting in the url_signature project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

url_signature's People

Contributors

dependabot[bot] avatar fnando avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

url_signature's Issues

Ideas

I've maintained a small class that we use in our system, that does something similar, for a while (copied below).

However, we'd be happy to switch to a gem and I can also try to help a bit with maintenance if you want (though this seem fairly feature complete, so I don't think there is much work needed really).

There are some minor differences though:

  • We sort query params (makes it easier for a client to build urls dynamically, without considering query param order)
  • We sign only the path + query params, i.e. not the origin (origin is authenticated by other means, HTTPS, in our use-case)
  • We also use another HMAC, but that's already configurable.

So my question is, would you be open to a PR that added these two things as opt-in features?

Our class

class Signer
    SIGNING_ALGORITHM = 'sha1'
    SIGNATURE_PARAM_NAME = 'signature'
    SPLIT_SIGNATURE_PATTERN = /&?signature=/ # for both "signature=abc" and "max-width=100&signature=abc"

    class InvalidSignature < StandardError; end

    def initialize(secret)
      @secret = secret
    end

    # Sign a URL
    # Only the path and query is signed, making it easier to change domains.
    #
    # @param url [String] unsigned url
    # @return [String] signed url
    def sign_url(url)
      raise ArgumentError unless url.is_a?(String)

      parsed = Addressable::URI.parse(url)

      if (parsed.query_values || {}).keys.include?(SIGNATURE_PARAM_NAME)
        raise ArgumentError, 'Cannot sign an url with query param `signature`'
      end

      # sort query params, based on encoded keys
      query_str = (parsed.query || '').split('&').sort.join('&')

      # join together the signed string (path + query params), in raw format (i.e. url-encoded)
      evaluated_string = (query_str.length > 0) ? "#{parsed.path}?#{query_str}" : parsed.path
      signature = compute_signature(evaluated_string)

      # append signature, should be last
      new_query = parsed.query || ''
      new_query += '&' unless new_query.length == 0
      new_query += "#{SIGNATURE_PARAM_NAME}=#{signature}"

      parsed.query = new_query
      parsed.to_s # return the signed url
    end

    # Verify a signed url
    #
    # @return [String] url with query-param signature, e.g. ?signature=abc
    def verify_signed_url!(url)
      raise ArgumentError unless url.is_a?(String)

      parsed = Addressable::URI.parse(url)

      unless (parsed.query_values || {}).keys.include?(SIGNATURE_PARAM_NAME)
        raise InvalidSignature, 'Missing signature query param'
      end

      # needed since we are using String#split below
      unless (parsed.query_values || {}).keys.last == SIGNATURE_PARAM_NAME
        raise InvalidSignature, 'Expected signature query param to be last'
      end

      # extract the signature from query string, leaving the original query string
      sans_signature, signature = (parsed.query || '').split(SPLIT_SIGNATURE_PATTERN)

      # sort query string before validating signature
      query_str = sans_signature.split('&').sort.join('&')

      # build a string with path plus query, e.g. `/files/123?expire-at=2012-03-01&max-width=300`
      evaluated_string = (query_str.length > 0) ? "#{parsed.path}?#{query_str}" : parsed.path

      unless compute_signature(evaluated_string) == signature
        raise InvalidSignature, 'Invalid signature provided'
      end
    end

    private

    def secret
      @secret
    end

    def compute_signature(string)
      OpenSSL::HMAC.hexdigest(SIGNING_ALGORITHM, secret, string)
    end
  end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.