├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENCE ├── README.md ├── bin └── test ├── lib ├── signed_params.rb └── signed_params │ ├── railtie.rb │ └── version.rb ├── signed_params.gemspec └── test ├── signed_params_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | tmp/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### Unreleased 4 | 5 | - feature: Exposed `params.signed` and `params.sign` - #2 [BREAKING CHANGE] 6 | 7 | ### 0.1.0 8 | 9 | - The initial release! 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "debug" 6 | gem "railties" 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | signed_params (0.1.0) 5 | actionpack (>= 6.1) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actionpack (8.0.2) 11 | actionview (= 8.0.2) 12 | activesupport (= 8.0.2) 13 | nokogiri (>= 1.8.5) 14 | rack (>= 2.2.4) 15 | rack-session (>= 1.0.1) 16 | rack-test (>= 0.6.3) 17 | rails-dom-testing (~> 2.2) 18 | rails-html-sanitizer (~> 1.6) 19 | useragent (~> 0.16) 20 | actionview (8.0.2) 21 | activesupport (= 8.0.2) 22 | builder (~> 3.1) 23 | erubi (~> 1.11) 24 | rails-dom-testing (~> 2.2) 25 | rails-html-sanitizer (~> 1.6) 26 | activesupport (8.0.2) 27 | base64 28 | benchmark (>= 0.3) 29 | bigdecimal 30 | concurrent-ruby (~> 1.0, >= 1.3.1) 31 | connection_pool (>= 2.2.5) 32 | drb 33 | i18n (>= 1.6, < 2) 34 | logger (>= 1.4.2) 35 | minitest (>= 5.1) 36 | securerandom (>= 0.3) 37 | tzinfo (~> 2.0, >= 2.0.5) 38 | uri (>= 0.13.1) 39 | base64 (0.3.0) 40 | benchmark (0.4.1) 41 | bigdecimal (3.2.0) 42 | builder (3.3.0) 43 | concurrent-ruby (1.3.5) 44 | connection_pool (2.5.3) 45 | crass (1.0.6) 46 | date (3.4.1) 47 | debug (1.10.0) 48 | irb (~> 1.10) 49 | reline (>= 0.3.8) 50 | drb (2.2.3) 51 | erb (5.0.1) 52 | erubi (1.13.1) 53 | i18n (1.14.7) 54 | concurrent-ruby (~> 1.0) 55 | io-console (0.8.0) 56 | irb (1.15.2) 57 | pp (>= 0.6.0) 58 | rdoc (>= 4.0.0) 59 | reline (>= 0.4.2) 60 | logger (1.7.0) 61 | loofah (2.24.1) 62 | crass (~> 1.0.2) 63 | nokogiri (>= 1.12.0) 64 | minitest (5.25.5) 65 | nokogiri (1.18.8-aarch64-linux-gnu) 66 | racc (~> 1.4) 67 | nokogiri (1.18.8-aarch64-linux-musl) 68 | racc (~> 1.4) 69 | nokogiri (1.18.8-arm-linux-gnu) 70 | racc (~> 1.4) 71 | nokogiri (1.18.8-arm-linux-musl) 72 | racc (~> 1.4) 73 | nokogiri (1.18.8-arm64-darwin) 74 | racc (~> 1.4) 75 | nokogiri (1.18.8-x86_64-darwin) 76 | racc (~> 1.4) 77 | nokogiri (1.18.8-x86_64-linux-gnu) 78 | racc (~> 1.4) 79 | nokogiri (1.18.8-x86_64-linux-musl) 80 | racc (~> 1.4) 81 | pp (0.6.2) 82 | prettyprint 83 | prettyprint (0.2.0) 84 | psych (5.2.6) 85 | date 86 | stringio 87 | racc (1.8.1) 88 | rack (3.1.15) 89 | rack-session (2.1.1) 90 | base64 (>= 0.1.0) 91 | rack (>= 3.0.0) 92 | rack-test (2.2.0) 93 | rack (>= 1.3) 94 | rackup (2.2.1) 95 | rack (>= 3) 96 | rails-dom-testing (2.3.0) 97 | activesupport (>= 5.0.0) 98 | minitest 99 | nokogiri (>= 1.6) 100 | rails-html-sanitizer (1.6.2) 101 | loofah (~> 2.21) 102 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 103 | railties (8.0.2) 104 | actionpack (= 8.0.2) 105 | activesupport (= 8.0.2) 106 | irb (~> 1.13) 107 | rackup (>= 1.0.0) 108 | rake (>= 12.2) 109 | thor (~> 1.0, >= 1.2.2) 110 | zeitwerk (~> 2.6) 111 | rake (13.3.0) 112 | rdoc (6.14.0) 113 | erb 114 | psych (>= 4.0.0) 115 | reline (0.6.1) 116 | io-console (~> 0.5) 117 | securerandom (0.4.1) 118 | stringio (3.1.7) 119 | thor (1.3.2) 120 | tzinfo (2.0.6) 121 | concurrent-ruby (~> 1.0) 122 | uri (1.0.3) 123 | useragent (0.16.11) 124 | zeitwerk (2.7.3) 125 | 126 | PLATFORMS 127 | aarch64-linux-gnu 128 | aarch64-linux-musl 129 | arm-linux-gnu 130 | arm-linux-musl 131 | arm64-darwin 132 | x86_64-darwin 133 | x86_64-linux-gnu 134 | x86_64-linux-musl 135 | 136 | DEPENDENCIES 137 | debug 138 | railties 139 | signed_params! 140 | 141 | BUNDLED WITH 142 | 2.6.9 143 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Elvinas Predkelis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Signed Params 2 | 3 | A lightweight library for encoding/decoding Rails request parameters. 4 | 5 | `signed_params` are protected against tampering and safe to share with the internet. Great for generating sharable links and/or mitigating web scrapers. 6 | 7 | Battle-tested at [Hansa](https://hansahq.com). Developed at [Primevise](https://primevise.com). 8 | 9 | signed_params GEM Version 10 | signed_params GEM Downloads 11 | 12 | --- 13 | 14 | ## Installation 15 | 16 | #### Add gem 17 | 18 | Simply add the gem to your Gemfile by running the following command 19 | 20 | ``` 21 | bundle add signed_params 22 | ``` 23 | 24 | --- 25 | 26 | ## Usage 27 | 28 | The signed paramaters can be accesed via `params.signed`. It mirrors the behavior of Rails' [signed cookies](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html). 29 | 30 | Similarly, setting a signed parameter can be done with the `params.sign` method. 31 | 32 | #### Example 33 | 34 | ```ruby 35 | class RecordsController < ApplicationController 36 | def index 37 | 38 | # Using `params.signed` will return `nil` if the parameter is tampered 39 | record_ids = params.signed[:record_ids] 40 | 41 | # Using `params.signed.fetch` will raise `ActionController::Parameters::InvalidSignature` if the parameter is tampered 42 | record_ids = params.signed.fetch(:record_ids) 43 | 44 | @records = Record.find(record_ids) 45 | end 46 | 47 | def new_public_link 48 | record_ids = Record.last(8).pluck(:id) 49 | redirect_to records_path(params.sign(record_ids:)) 50 | end 51 | end 52 | ``` 53 | 54 | > [!TIP] 55 | > You can use all sorts of datatypes when signing parameters. Strings, integers, arrays, objects - they all just work. 56 | 57 | > [!CAUTION] 58 | > Avoid exposing sensitive data while using `signed_params`. Your application should still implement proper authentication and authorization. 59 | 60 | --- 61 | 62 | ## License 63 | 64 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 65 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.expand_path("../test", __dir__) 3 | 4 | require "bundler/setup" 5 | require "rails/plugin/test" 6 | -------------------------------------------------------------------------------- /lib/signed_params.rb: -------------------------------------------------------------------------------- 1 | require "signed_params/version" 2 | require "action_controller/metal/strong_parameters" 3 | 4 | class ActionController::Parameters::Signed < Data.define(:verifier, :params) 5 | ActionController::Parameters::InvalidSignature = Class.new StandardError 6 | 7 | def [](key) 8 | verifier.verified(params[key]) 9 | end 10 | 11 | def fetch(key) 12 | verifier.verify(params[key]) 13 | rescue ActiveSupport::MessageVerifier::InvalidSignature 14 | raise ActionController::Parameters::InvalidSignature 15 | end 16 | end 17 | 18 | module ActionController::Parameters::Signed::Integration 19 | def self.included(parameters) 20 | parameters.mattr_accessor :verifier 21 | end 22 | 23 | def sign(**params) 24 | params.transform_values { verifier.generate _1 } 25 | end 26 | def signed = @signed ||= ActionController::Parameters::Signed.new(verifier, self) 27 | end 28 | 29 | require_relative "signed_params/railtie" if defined?(Rails::Railtie) 30 | -------------------------------------------------------------------------------- /lib/signed_params/railtie.rb: -------------------------------------------------------------------------------- 1 | class SignedParams::Railtie < Rails::Railtie 2 | initializer "parameters.signed.set_verifier" do |app| 3 | ActionController::Parameters.include ActionController::Parameters::Signed::Integration 4 | ActionController::Parameters.verifier = app.message_verifier :signed_parameters 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/signed_params/version.rb: -------------------------------------------------------------------------------- 1 | module SignedParams 2 | VERSION = "0.2.0" 3 | end 4 | -------------------------------------------------------------------------------- /signed_params.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/signed_params/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "signed_params" 5 | spec.version = SignedParams::VERSION 6 | spec.authors = ["Elvinas Predkelis"] 7 | spec.email = ["elvinas@primevise.com"] 8 | spec.homepage = "https://github.com/elvinaspredkelis/signed_params" 9 | spec.summary = "A lightweight library for encoding/decoding Rails request parameters." 10 | spec.description = "A lightweight library for encoding/decoding Rails request parameters." 11 | spec.license = "MIT" 12 | spec.required_ruby_version = ">= 3.0.0" 13 | 14 | spec.metadata["homepage_uri"] = spec.homepage 15 | spec.metadata["source_code_uri"] = "https://github.com/elvinaspredkelis/signed_params" 16 | spec.metadata["changelog_uri"] = "https://github.com/elvinaspredkelis/signed_params/blob/main/CHANGELOG.md" 17 | 18 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 19 | Dir["{lib}/**/*", "LICENCE", "Rakefile", "README.md"] 20 | end 21 | 22 | spec.add_dependency "actionpack", ">= 6.1" 23 | end 24 | -------------------------------------------------------------------------------- /test/signed_params_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SignedParamsTest < ActionDispatch::IntegrationTest 4 | test "with nil id" do 5 | assert_nil params.signed[:id] 6 | assert_raises ActionController::Parameters::InvalidSignature do 7 | params.signed.fetch(:id) 8 | end 9 | end 10 | 11 | test "with signed parameter id" do 12 | signed = params.sign(id: 1) 13 | assert_includes signed[:id], "==--" 14 | 15 | params = ActionController::Parameters.new(**signed) 16 | assert_equal 1, params.signed[:id] 17 | assert_equal 1, params.signed.fetch(:id) 18 | end 19 | 20 | test "pass signed params to URLs" do 21 | uri = URI post_url params.sign(id: 1, first_query_param: true, second_query_param: false) 22 | assert_match(/posts\/.*?==--/, uri.path) 23 | 24 | query = Rack::Utils.parse_query uri.query 25 | assert_equal 2, query.keys.size 26 | assert_match "--", query.fetch("first_query_param") 27 | assert_match "--", query.fetch("second_query_param") 28 | end 29 | 30 | test "extract signed params from controller" do 31 | get post_url(params.sign(id: 1)) 32 | assert_equal 1, response.body.to_i 33 | end 34 | 35 | private 36 | def params = ActionController::Parameters.new 37 | end 38 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "rails" 2 | require "rails/application" 3 | require "action_controller" 4 | require "action_controller/test_case" 5 | 6 | require "signed_params" 7 | 8 | class SignedParams::Application < Rails::Application 9 | end 10 | 11 | SignedParams::Railtie.run_initializers :default, Rails.application 12 | 13 | Rails.logger = Logger.new "/dev/null" 14 | 15 | class PostsController < ActionController::Base 16 | def show 17 | render plain: params.signed[:id] 18 | end 19 | end 20 | 21 | Rails.application.routes.draw do 22 | resources :posts 23 | end 24 | 25 | class ActionDispatch::IntegrationTest 26 | include Rails.application.routes.url_helpers 27 | setup { @app = Rails.application } 28 | end 29 | --------------------------------------------------------------------------------